@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.
- package/README.md +1 -1
- package/bin/gtkx-mcp.js +2 -2
- package/dist/app-router.d.ts +3 -5
- package/dist/app-router.d.ts.map +1 -1
- package/dist/app-router.js +21 -34
- package/dist/app-router.js.map +1 -1
- package/dist/connection-registry.d.ts +3 -4
- package/dist/connection-registry.d.ts.map +1 -1
- package/dist/connection-registry.js +5 -16
- package/dist/connection-registry.js.map +1 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/protocol/errors.d.ts +6 -7
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +12 -8
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/schemas.d.ts +1 -19
- package/dist/protocol/schemas.d.ts.map +1 -1
- package/dist/protocol/schemas.js +1 -16
- package/dist/protocol/schemas.js.map +1 -1
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +8 -5
- package/dist/reference.js.map +1 -1
- package/dist/server.d.ts +8 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +79 -18
- package/dist/server.js.map +1 -1
- package/dist/socket-server.js +1 -1
- package/dist/socket-server.js.map +1 -1
- package/dist/tool-filter.d.ts +5 -0
- package/dist/tool-filter.d.ts.map +1 -0
- package/dist/tool-filter.js +38 -0
- package/dist/tool-filter.js.map +1 -0
- package/dist/tool.d.ts +1 -0
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +1 -1
- package/dist/tool.js.map +1 -1
- package/dist/transport.d.ts +18 -39
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +78 -127
- package/dist/transport.js.map +1 -1
- package/package.json +9 -5
- package/src/app-router.ts +34 -44
- package/src/connection-registry.ts +6 -22
- package/src/internal.ts +2 -3
- package/src/protocol/errors.ts +20 -10
- package/src/protocol/schemas.ts +0 -41
- package/src/reference.ts +8 -5
- package/src/server.ts +116 -19
- package/src/socket-server.ts +1 -1
- package/src/tool-filter.ts +54 -0
- package/src/tool.ts +2 -1
- package/src/transport.ts +106 -171
package/src/app-router.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
+
import { type JSONRPCRequest, McpError, type Result } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import type { AppConnections, ConnectionEvent, ProtocolConnection, RequestParams } from "./transport.js";
|
|
1
3
|
import {
|
|
2
4
|
appNotFoundError,
|
|
5
|
+
CONNECTION_CLOSED_CODE,
|
|
3
6
|
connectionWriteFailedError,
|
|
4
7
|
invalidRequestError,
|
|
5
8
|
methodNotFoundError,
|
|
6
9
|
noAppConnectedError,
|
|
7
10
|
type ProtocolError,
|
|
11
|
+
protocolErrorFrom,
|
|
12
|
+
REQUEST_TIMEOUT_CODE,
|
|
13
|
+
requestTimeoutError,
|
|
8
14
|
} from "./protocol/errors.js";
|
|
9
|
-
import { type AppInfo, RegisterParamsSchema
|
|
10
|
-
import {
|
|
11
|
-
type AppConnections,
|
|
12
|
-
ConnectionClosedError,
|
|
13
|
-
type ConnectionEvent,
|
|
14
|
-
type ConnectionRequestEvent,
|
|
15
|
-
type ProtocolConnection,
|
|
16
|
-
} from "./transport.js";
|
|
15
|
+
import { type AppInfo, RegisterParamsSchema } from "./protocol/schemas.js";
|
|
17
16
|
|
|
18
17
|
type AppRegisteredEvent = CustomEvent<AppInfo>;
|
|
19
18
|
type AppUnregisteredEvent = CustomEvent<string>;
|
|
@@ -48,11 +47,7 @@ class AppRouter extends EventTarget {
|
|
|
48
47
|
super();
|
|
49
48
|
this.connections = connections;
|
|
50
49
|
this.requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
51
|
-
|
|
52
|
-
this.connections.addEventListener("request", (event) => {
|
|
53
|
-
const { connection, request } = (event as ConnectionRequestEvent).detail;
|
|
54
|
-
this.handleRequest(connection, request);
|
|
55
|
-
});
|
|
50
|
+
this.connections.onRequest = (connection, request) => this.handleRequest(connection, request);
|
|
56
51
|
|
|
57
52
|
this.connections.addEventListener("disconnection", (event) => {
|
|
58
53
|
this.removeApp((event as ConnectionEvent).detail);
|
|
@@ -73,23 +68,25 @@ class AppRouter extends EventTarget {
|
|
|
73
68
|
throw noAppConnectedError();
|
|
74
69
|
}
|
|
75
70
|
|
|
76
|
-
private handleRequest(connection: ProtocolConnection, request:
|
|
71
|
+
private handleRequest(connection: ProtocolConnection, request: JSONRPCRequest): Promise<Result> {
|
|
77
72
|
if (request.method === "app.register") {
|
|
78
|
-
this.handleRegister(connection, request);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.
|
|
73
|
+
return Promise.resolve(this.handleRegister(connection, request));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (request.method === "app.unregister") {
|
|
77
|
+
this.removeApp(connection);
|
|
78
|
+
|
|
79
|
+
return Promise.resolve({ success: true });
|
|
83
80
|
}
|
|
81
|
+
|
|
82
|
+
return Promise.reject(methodNotFoundError(request.method));
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
private handleRegister(connection: ProtocolConnection, request:
|
|
85
|
+
private handleRegister(connection: ProtocolConnection, request: JSONRPCRequest): Result {
|
|
87
86
|
const parseResult = RegisterParamsSchema.safeParse(request.params);
|
|
88
87
|
|
|
89
88
|
if (!parseResult.success) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return;
|
|
89
|
+
throw invalidRequestError(parseResult.error.message);
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
const params = parseResult.data;
|
|
@@ -102,29 +99,23 @@ class AppRouter extends EventTarget {
|
|
|
102
99
|
|
|
103
100
|
this.apps.set(params.applicationId, { info: appInfo, connection });
|
|
104
101
|
this.connectionToApp.set(connection.id, params.applicationId);
|
|
105
|
-
this.acknowledge(connection, request);
|
|
106
102
|
this.dispatchEvent(appRegisteredEvent(appInfo));
|
|
107
|
-
}
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
this.removeApp(connection);
|
|
111
|
-
this.acknowledge(connection, request);
|
|
104
|
+
return { success: true };
|
|
112
105
|
}
|
|
113
106
|
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
result: { success: true },
|
|
118
|
-
};
|
|
107
|
+
private toAppError(app: RegisteredApp, error: McpError): ProtocolError {
|
|
108
|
+
if (error.code === CONNECTION_CLOSED_CODE) {
|
|
109
|
+
this.removeApp(app.connection);
|
|
119
110
|
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
return connectionWriteFailedError(app.info.applicationId);
|
|
112
|
+
}
|
|
122
113
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
114
|
+
if (error.code === REQUEST_TIMEOUT_CODE) {
|
|
115
|
+
return requestTimeoutError(this.requestTimeout);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return protocolErrorFrom(error);
|
|
128
119
|
}
|
|
129
120
|
|
|
130
121
|
private removeApp(connection: ProtocolConnection): void {
|
|
@@ -190,15 +181,14 @@ class AppRouter extends EventTarget {
|
|
|
190
181
|
});
|
|
191
182
|
}
|
|
192
183
|
|
|
193
|
-
async sendToApp<T>(applicationId: string | undefined, method: string, params?:
|
|
184
|
+
async sendToApp<T>(applicationId: string | undefined, method: string, params?: RequestParams): Promise<T> {
|
|
194
185
|
const app = this.resolveTargetApp(applicationId);
|
|
195
186
|
|
|
196
187
|
try {
|
|
197
188
|
return await app.connection.send<T>(method, params, this.requestTimeout);
|
|
198
189
|
} catch (error) {
|
|
199
|
-
if (error instanceof
|
|
200
|
-
this.
|
|
201
|
-
throw connectionWriteFailedError(app.info.applicationId);
|
|
190
|
+
if (error instanceof McpError) {
|
|
191
|
+
throw this.toAppError(app, error);
|
|
202
192
|
}
|
|
203
193
|
|
|
204
194
|
throw error;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Socket } from "node:net";
|
|
2
|
-
import
|
|
2
|
+
import { methodNotFoundError } from "./protocol/errors.js";
|
|
3
3
|
import {
|
|
4
4
|
type AppConnections,
|
|
5
5
|
connectionDisconnectionEvent,
|
|
6
6
|
connectionErrorEvent,
|
|
7
|
-
|
|
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.
|
|
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
|
-
|
|
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";
|
package/src/protocol/errors.ts
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
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
|
};
|
package/src/protocol/schemas.ts
CHANGED
|
@@ -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
|
};
|
package/src/reference.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ApiReference, type ApiSymbol, loadApiReference, resolveGirPath, resolveLibraries } from "@gtkx/codegen";
|
|
2
2
|
import { loadConfig } from "@gtkx/config";
|
|
3
|
+
import { resolveFuture } from "@gtkx/config/internal";
|
|
3
4
|
import { type McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
5
|
import { type CallToolResult, ErrorCode, McpError, type ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
6
|
import { existsSync, statSync } from "node:fs";
|
|
@@ -192,15 +193,17 @@ const loadReference = async (requestedRoot: string): Promise<LoadedReference> =>
|
|
|
192
193
|
);
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
const
|
|
196
|
+
const future = resolveFuture(config.future);
|
|
197
|
+
const libraries = resolveLibraries(config.libraries, girPath, future.isAdwaitaDefault);
|
|
196
198
|
|
|
197
199
|
const reference = loadApiReference({
|
|
198
200
|
libraries,
|
|
199
201
|
girPath,
|
|
200
|
-
isByteArrayTyped:
|
|
201
|
-
isValueUnwrapped:
|
|
202
|
-
isFinishTrimmed:
|
|
203
|
-
isInoutInPlace:
|
|
202
|
+
isByteArrayTyped: future.isByteArrayTyped,
|
|
203
|
+
isValueUnwrapped: future.isValueUnwrapped,
|
|
204
|
+
isFinishTrimmed: future.isFinishTrimmed,
|
|
205
|
+
isInoutInPlace: future.isInoutInPlace,
|
|
206
|
+
isTreeShaken: future.isTreeShaken,
|
|
204
207
|
});
|
|
205
208
|
|
|
206
209
|
const watched = [watchFile(resolve(root, configFile)), ...reference.girFiles.map((file) => watchFile(file))];
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { loadConfig } from "@gtkx/config";
|
|
3
|
+
import { type McpSettings, resolveMcpSettings } from "@gtkx/config/internal";
|
|
1
4
|
import { createLogger, installGracefulShutdown, type Logger } from "@gtkx/utils";
|
|
2
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
4
8
|
import { z } from "zod";
|
|
5
9
|
import type { ConnectionErrorEvent } from "./transport.js";
|
|
6
10
|
import packageManifest from "../package.json" with { type: "json" };
|
|
@@ -26,11 +30,19 @@ import {
|
|
|
26
30
|
registerReferenceResources,
|
|
27
31
|
} from "./reference.js";
|
|
28
32
|
import { SocketServer } from "./socket-server.js";
|
|
29
|
-
import {
|
|
33
|
+
import { selectTools } from "./tool-filter.js";
|
|
34
|
+
import { defineTool, imageContent, registerTool, textContent, textError, type Tool } from "./tool.js";
|
|
30
35
|
|
|
31
36
|
type CreateMcpServerOptions = {
|
|
32
37
|
socketPath?: string;
|
|
33
38
|
version: string;
|
|
39
|
+
settings?: McpSettings;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ServerOptions = {
|
|
43
|
+
cwd?: string;
|
|
44
|
+
tools?: string[];
|
|
45
|
+
isReadOnly?: boolean;
|
|
34
46
|
};
|
|
35
47
|
|
|
36
48
|
type McpServerHandle = {
|
|
@@ -51,6 +63,18 @@ type AppWithWindows = AppInfo & { windows?: AppWindow[] };
|
|
|
51
63
|
|
|
52
64
|
const { version } = packageManifest;
|
|
53
65
|
const log: Logger = createLogger("mcp");
|
|
66
|
+
const DEFAULT_SETTINGS: McpSettings = { tools: [], isReadOnly: false };
|
|
67
|
+
|
|
68
|
+
const INSTRUCTIONS =
|
|
69
|
+
"The widget tools drive a GTKX app running under `gtkx dev`: they read " +
|
|
70
|
+
"its live widget tree, query it by accessible role and name, click and type, and capture screenshots. " +
|
|
71
|
+
"They fail until an app is running, so start `gtkx dev` first. The reference tools answer from the " +
|
|
72
|
+
"bindings generated for a specific project, so they describe that project's GIR libraries rather than " +
|
|
73
|
+
"GTK in general; prefer them over recalled GTK knowledge, which is usually C, PyGObject or GJS and " +
|
|
74
|
+
"does not apply here.\n\n" +
|
|
75
|
+
"Widget IDs are valid only while the widget is mounted. After a dialog closes, a list re-renders, or " +
|
|
76
|
+
"fast refresh patches a component, re-read the tree or re-run the query instead of reusing an ID.";
|
|
77
|
+
|
|
54
78
|
const APPLICATION_ID_DESCRIPTION = "Application ID to query. If not specified, uses the first connected app.";
|
|
55
79
|
|
|
56
80
|
const WIDGET_ID_DESCRIPTION =
|
|
@@ -137,6 +161,14 @@ const screenshotShape = {
|
|
|
137
161
|
"Absolute path to write the PNG to on the app's machine. If set, the screenshot is saved there " +
|
|
138
162
|
"in addition to being returned.",
|
|
139
163
|
}),
|
|
164
|
+
returnImage: z
|
|
165
|
+
.boolean()
|
|
166
|
+
.optional()
|
|
167
|
+
.describe(
|
|
168
|
+
"Whether to return the PNG as image content, true by default. Pass false together with `path` " +
|
|
169
|
+
"to save the screenshot and get back only where it landed, which keeps the image out of the " +
|
|
170
|
+
"conversation until something actually needs to look at it.",
|
|
171
|
+
),
|
|
140
172
|
};
|
|
141
173
|
|
|
142
174
|
function describeParams<Shape extends Record<string, z.ZodType>>(
|
|
@@ -202,6 +234,28 @@ const listAppsTool = (appRouter: AppRouter): Tool =>
|
|
|
202
234
|
},
|
|
203
235
|
});
|
|
204
236
|
|
|
237
|
+
const screenshotResult = (
|
|
238
|
+
result: { data: string; mimeType: string; savedPath?: string },
|
|
239
|
+
shouldReturnImage: boolean,
|
|
240
|
+
): CallToolResult => {
|
|
241
|
+
if (result.savedPath === undefined) {
|
|
242
|
+
return shouldReturnImage
|
|
243
|
+
? imageContent(result.data, result.mimeType)
|
|
244
|
+
: textError(
|
|
245
|
+
"Nothing to return: `returnImage` was false and no `path` was given, so the screenshot was " +
|
|
246
|
+
"neither saved nor returned. Pass `path` to save it, or leave `returnImage` unset.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const saved = { type: "text", text: `Screenshot saved to ${result.savedPath}` } as const;
|
|
251
|
+
|
|
252
|
+
if (!shouldReturnImage) {
|
|
253
|
+
return { content: [saved] };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { content: [saved, { type: "image", data: result.data, mimeType: result.mimeType }] };
|
|
257
|
+
};
|
|
258
|
+
|
|
205
259
|
const screenshotTool = (appRouter: AppRouter): Tool =>
|
|
206
260
|
defineTool({
|
|
207
261
|
name: "gtkx_take_screenshot",
|
|
@@ -212,23 +266,14 @@ const screenshotTool = (appRouter: AppRouter): Tool =>
|
|
|
212
266
|
"the PNG to `path` on the app's machine. You can't target widgets from a screenshot; use " +
|
|
213
267
|
"`gtkx_get_widget_tree` to find widget IDs for interaction.",
|
|
214
268
|
inputSchema: screenshotShape,
|
|
215
|
-
handler: async ({ applicationId, ...params }) => {
|
|
269
|
+
handler: async ({ applicationId, returnImage, ...params }) => {
|
|
216
270
|
const result = await appRouter.sendToApp<{ data: string; mimeType: string; savedPath?: string }>(
|
|
217
271
|
applicationId,
|
|
218
272
|
"widget.screenshot",
|
|
219
273
|
params,
|
|
220
274
|
);
|
|
221
275
|
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
content: [
|
|
225
|
-
{ type: "text", text: `Screenshot saved to ${result.savedPath}` },
|
|
226
|
-
{ type: "image", data: result.data, mimeType: result.mimeType },
|
|
227
|
-
],
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return imageContent(result.data, result.mimeType);
|
|
276
|
+
return screenshotResult(result, returnImage !== false);
|
|
232
277
|
},
|
|
233
278
|
});
|
|
234
279
|
|
|
@@ -348,8 +393,19 @@ function buildTools(appRouter: AppRouter): Tool[] {
|
|
|
348
393
|
return [...buildInspectionTools(appRouter), ...buildInteractionTools(appRouter)];
|
|
349
394
|
}
|
|
350
395
|
|
|
351
|
-
const registerTools = (
|
|
352
|
-
|
|
396
|
+
const registerTools = (
|
|
397
|
+
mcpServer: McpServer,
|
|
398
|
+
appRouter: AppRouter,
|
|
399
|
+
provider: ReferenceProvider,
|
|
400
|
+
settings: McpSettings,
|
|
401
|
+
): void => {
|
|
402
|
+
const tools = selectTools([...buildTools(appRouter), ...buildReferenceTools(provider)], settings);
|
|
403
|
+
|
|
404
|
+
if (tools.length === 0) {
|
|
405
|
+
log.warn("no tools matched the configured filter; the server is registering none");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const tool of tools) {
|
|
353
409
|
registerTool(mcpServer, tool);
|
|
354
410
|
}
|
|
355
411
|
};
|
|
@@ -402,16 +458,57 @@ const createMcpServer = (options: CreateMcpServerOptions): McpServerHandle => {
|
|
|
402
458
|
log.info(`app unregistered: ${(event as AppUnregisteredEvent).detail}`);
|
|
403
459
|
});
|
|
404
460
|
|
|
405
|
-
const mcpServer = new McpServer({ name: "gtkx-mcp", version: options.version });
|
|
461
|
+
const mcpServer = new McpServer({ name: "gtkx-mcp", version: options.version }, { instructions: INSTRUCTIONS });
|
|
406
462
|
const referenceProvider = createReferenceProvider({ getAppRoot: () => appRouter.getProjectRoot() });
|
|
407
|
-
registerTools(mcpServer, appRouter, referenceProvider);
|
|
463
|
+
registerTools(mcpServer, appRouter, referenceProvider, options.settings ?? DEFAULT_SETTINGS);
|
|
408
464
|
registerReferenceResources(mcpServer, referenceProvider);
|
|
409
465
|
|
|
410
466
|
return createServerHandle(socketServer, mcpServer, socketPath);
|
|
411
467
|
};
|
|
412
468
|
|
|
413
|
-
async
|
|
414
|
-
|
|
469
|
+
const configuredSettings = async (cwd: string): Promise<McpSettings> => {
|
|
470
|
+
try {
|
|
471
|
+
const { config } = await loadConfig(cwd);
|
|
472
|
+
|
|
473
|
+
return resolveMcpSettings(config);
|
|
474
|
+
} catch {
|
|
475
|
+
return DEFAULT_SETTINGS;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const resolveSettings = async (options: ServerOptions): Promise<McpSettings> => {
|
|
480
|
+
const configured = await configuredSettings(options.cwd ?? process.cwd());
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
tools: options.tools ?? configured.tools,
|
|
484
|
+
isReadOnly: options.isReadOnly ?? configured.isReadOnly,
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const splitPatterns = (values: string[]): string[] =>
|
|
489
|
+
values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
490
|
+
|
|
491
|
+
const parseServerArgs = (argv: string[]): ServerOptions => {
|
|
492
|
+
const { values } = parseArgs({
|
|
493
|
+
args: argv,
|
|
494
|
+
options: {
|
|
495
|
+
tools: { type: "string", multiple: true },
|
|
496
|
+
"read-only": { type: "boolean" },
|
|
497
|
+
},
|
|
498
|
+
allowPositionals: false,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const tools = values.tools === undefined ? undefined : splitPatterns(values.tools);
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
...(tools !== undefined && { tools }),
|
|
505
|
+
...(values["read-only"] !== undefined && { isReadOnly: values["read-only"] }),
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
async function main(options: ServerOptions = {}): Promise<void> {
|
|
510
|
+
const settings = await resolveSettings(options);
|
|
511
|
+
const server = createMcpServer({ version, settings });
|
|
415
512
|
|
|
416
513
|
installGracefulShutdown({
|
|
417
514
|
onSignal: () => server.stop(),
|
|
@@ -420,4 +517,4 @@ async function main(): Promise<void> {
|
|
|
420
517
|
await server.start();
|
|
421
518
|
}
|
|
422
519
|
|
|
423
|
-
export { log, main };
|
|
520
|
+
export { log, main, main as runMcpServer, parseServerArgs };
|
package/src/socket-server.ts
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { McpSettings } from "@gtkx/config/internal";
|
|
2
|
+
import type { Tool } from "./tool.js";
|
|
3
|
+
|
|
4
|
+
const NEGATION_PREFIX = "!";
|
|
5
|
+
const SPECIAL_CHARACTERS = /[.+?^${}()|[\]\\]/g;
|
|
6
|
+
|
|
7
|
+
const escapeLiteral = (value: string): string => value.replaceAll(SPECIAL_CHARACTERS, String.raw`\$&`);
|
|
8
|
+
|
|
9
|
+
const patternToRegExp = (pattern: string): RegExp =>
|
|
10
|
+
new RegExp(`^${pattern.split("*").map((part) => escapeLiteral(part)).join(".*")}$`);
|
|
11
|
+
|
|
12
|
+
const matchingNames = (tools: Tool[], pattern: string): string[] => {
|
|
13
|
+
const expression = patternToRegExp(pattern);
|
|
14
|
+
|
|
15
|
+
return tools.filter((tool) => expression.test(tool.name)).map((tool) => tool.name);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const applyPattern = (selected: Set<string>, tools: Tool[], pattern: string): void => {
|
|
19
|
+
const isNegated = pattern.startsWith(NEGATION_PREFIX);
|
|
20
|
+
const names = matchingNames(tools, isNegated ? pattern.slice(NEGATION_PREFIX.length) : pattern);
|
|
21
|
+
|
|
22
|
+
for (const name of names) {
|
|
23
|
+
if (isNegated) {
|
|
24
|
+
selected.delete(name);
|
|
25
|
+
} else {
|
|
26
|
+
selected.add(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const selectNames = (tools: Tool[], patterns: string[]): Set<string> => {
|
|
32
|
+
const isSubtractive = patterns.every((pattern) => pattern.startsWith(NEGATION_PREFIX));
|
|
33
|
+
const selected: Set<string> = new Set(isSubtractive ? tools.map((tool) => tool.name) : []);
|
|
34
|
+
|
|
35
|
+
for (const pattern of patterns) {
|
|
36
|
+
applyPattern(selected, tools, pattern);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return selected;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const selectTools = (tools: Tool[], settings: McpSettings): Tool[] => {
|
|
43
|
+
const allowed = settings.isReadOnly ? tools.filter((tool) => tool.kind === "readOnly") : tools;
|
|
44
|
+
|
|
45
|
+
if (settings.tools.length === 0) {
|
|
46
|
+
return allowed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const selected = selectNames(allowed, settings.tools);
|
|
50
|
+
|
|
51
|
+
return allowed.filter((tool) => selected.has(tool.name));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { selectTools };
|
package/src/tool.ts
CHANGED
|
@@ -12,6 +12,7 @@ type Tool<Shape extends Record<string, z.ZodType> = Record<string, z.ZodType>> =
|
|
|
12
12
|
kind: ToolKind;
|
|
13
13
|
description: string;
|
|
14
14
|
inputSchema: Shape;
|
|
15
|
+
isOpenWorld?: boolean;
|
|
15
16
|
handler: (args: ToolArgs<Shape>) => Promise<CallToolResult>;
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -63,7 +64,7 @@ const registerTool = (server: McpServer, tool: Tool): void => {
|
|
|
63
64
|
title: tool.title,
|
|
64
65
|
readOnlyHint: tool.kind === "readOnly",
|
|
65
66
|
destructiveHint: tool.kind === "action",
|
|
66
|
-
openWorldHint: true,
|
|
67
|
+
openWorldHint: tool.isOpenWorld === true,
|
|
67
68
|
},
|
|
68
69
|
},
|
|
69
70
|
callback,
|