@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/src/protocol/errors.ts
CHANGED
|
@@ -1,61 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
QUERY_TIMEOUT = 1005,
|
|
17
|
-
/** Widget is not the expected type */
|
|
18
|
-
INVALID_WIDGET_TYPE = 1006,
|
|
19
|
-
/** Screenshot capture failed */
|
|
20
|
-
SCREENSHOT_FAILED = 1007,
|
|
21
|
-
/** IPC request timed out */
|
|
22
|
-
IPC_TIMEOUT = 1008,
|
|
23
|
-
/** Failed to serialize data */
|
|
24
|
-
SERIALIZATION_ERROR = 1009,
|
|
25
|
-
/** Request format is invalid */
|
|
26
|
-
INVALID_REQUEST = 1010,
|
|
27
|
-
/** Requested method does not exist */
|
|
28
|
-
METHOD_NOT_FOUND = 1011,
|
|
1
|
+
export const ErrorCode = {
|
|
2
|
+
INTERNAL_ERROR: 1000,
|
|
3
|
+
NO_APP_CONNECTED: 1001,
|
|
4
|
+
APP_NOT_FOUND: 1002,
|
|
5
|
+
WIDGET_NOT_FOUND: 1003,
|
|
6
|
+
CONNECTION_WRITE_FAILED: 1004,
|
|
7
|
+
REQUEST_TIMEOUT: 1005,
|
|
8
|
+
INVALID_REQUEST: 1006,
|
|
9
|
+
METHOD_NOT_FOUND: 1007,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
13
|
+
|
|
14
|
+
export function isErrorCode(code: number): code is ErrorCode {
|
|
15
|
+
return (Object.values(ErrorCode) as number[]).includes(code);
|
|
29
16
|
}
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* Contains an error code, message, and optional additional data.
|
|
35
|
-
*/
|
|
36
|
-
export class McpError extends Error {
|
|
37
|
-
/** The MCP error code */
|
|
38
|
-
readonly code: McpErrorCode;
|
|
39
|
-
/** Additional error context */
|
|
40
|
-
readonly data?: unknown;
|
|
18
|
+
export class ProtocolError extends Error {
|
|
19
|
+
code: ErrorCode;
|
|
20
|
+
data?: unknown;
|
|
41
21
|
|
|
42
|
-
constructor(code:
|
|
22
|
+
constructor(code: ErrorCode, message: string, data?: unknown) {
|
|
43
23
|
super(message);
|
|
44
24
|
this.code = code;
|
|
45
25
|
this.data = data;
|
|
46
|
-
this.name = "
|
|
47
|
-
|
|
48
|
-
if (Error.captureStackTrace) {
|
|
49
|
-
Error.captureStackTrace(this, McpError);
|
|
50
|
-
}
|
|
26
|
+
this.name = "ProtocolError";
|
|
51
27
|
}
|
|
52
28
|
|
|
53
|
-
|
|
54
|
-
* Converts the error to an IPC-compatible format.
|
|
55
|
-
*
|
|
56
|
-
* @returns Object suitable for IPC response error field
|
|
57
|
-
*/
|
|
58
|
-
toIpcError(): { code: number; message: string; data?: unknown } {
|
|
29
|
+
toErrorObject(): { code: number; message: string; data?: unknown } {
|
|
59
30
|
return {
|
|
60
31
|
code: this.code,
|
|
61
32
|
message: this.message,
|
|
@@ -64,65 +35,38 @@ export class McpError extends Error {
|
|
|
64
35
|
}
|
|
65
36
|
}
|
|
66
37
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* @returns McpError with NO_APP_CONNECTED code
|
|
71
|
-
*/
|
|
72
|
-
export function noAppConnectedError(): McpError {
|
|
73
|
-
return new McpError(
|
|
74
|
-
McpErrorCode.NO_APP_CONNECTED,
|
|
38
|
+
export function noAppConnectedError(): ProtocolError {
|
|
39
|
+
return new ProtocolError(
|
|
40
|
+
ErrorCode.NO_APP_CONNECTED,
|
|
75
41
|
"No GTKX application connected: start an app with 'gtkx dev' to connect",
|
|
76
|
-
{ hint: "Run 'gtkx dev
|
|
42
|
+
{ hint: "Run 'gtkx dev' in your project directory" },
|
|
77
43
|
);
|
|
78
44
|
}
|
|
79
45
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
46
|
+
export function appNotFoundError(applicationId: string): ProtocolError {
|
|
47
|
+
return new ProtocolError(ErrorCode.APP_NOT_FOUND, `Application '${applicationId}' not found`, { applicationId });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function connectionWriteFailedError(applicationId: string): ProtocolError {
|
|
51
|
+
return new ProtocolError(
|
|
52
|
+
ErrorCode.CONNECTION_WRITE_FAILED,
|
|
53
|
+
`Connection to application '${applicationId}' is no longer writable`,
|
|
54
|
+
{ applicationId },
|
|
55
|
+
);
|
|
88
56
|
}
|
|
89
57
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
* @param widgetId - The widget ID that was not found
|
|
94
|
-
* @returns McpError with WIDGET_NOT_FOUND code
|
|
95
|
-
*/
|
|
96
|
-
export function widgetNotFoundError(widgetId: string): McpError {
|
|
97
|
-
return new McpError(McpErrorCode.WIDGET_NOT_FOUND, `Widget '${widgetId}' not found`, { widgetId });
|
|
58
|
+
export function widgetNotFoundError(widgetId: string): ProtocolError {
|
|
59
|
+
return new ProtocolError(ErrorCode.WIDGET_NOT_FOUND, `Widget '${widgetId}' not found`, { widgetId });
|
|
98
60
|
}
|
|
99
61
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*
|
|
103
|
-
* @param timeout - The timeout duration in milliseconds
|
|
104
|
-
* @returns McpError with IPC_TIMEOUT code
|
|
105
|
-
*/
|
|
106
|
-
export function ipcTimeoutError(timeout: number): McpError {
|
|
107
|
-
return new McpError(McpErrorCode.IPC_TIMEOUT, `IPC request timed out after ${timeout}ms`, { timeout });
|
|
62
|
+
export function requestTimeoutError(timeout: number): ProtocolError {
|
|
63
|
+
return new ProtocolError(ErrorCode.REQUEST_TIMEOUT, `Request timed out after ${timeout}ms`, { timeout });
|
|
108
64
|
}
|
|
109
65
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*
|
|
113
|
-
* @param reason - Description of why the request is invalid
|
|
114
|
-
* @returns McpError with INVALID_REQUEST code
|
|
115
|
-
*/
|
|
116
|
-
export function invalidRequestError(reason: string): McpError {
|
|
117
|
-
return new McpError(McpErrorCode.INVALID_REQUEST, `Invalid request: ${reason}`, { reason });
|
|
66
|
+
export function invalidRequestError(reason: string): ProtocolError {
|
|
67
|
+
return new ProtocolError(ErrorCode.INVALID_REQUEST, `Invalid request: ${reason}`, { reason });
|
|
118
68
|
}
|
|
119
69
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
*
|
|
123
|
-
* @param method - The method name that was not found
|
|
124
|
-
* @returns McpError with METHOD_NOT_FOUND code
|
|
125
|
-
*/
|
|
126
|
-
export function methodNotFoundError(method: string): McpError {
|
|
127
|
-
return new McpError(McpErrorCode.METHOD_NOT_FOUND, `Method '${method}' not found`, { method });
|
|
70
|
+
export function methodNotFoundError(method: string): ProtocolError {
|
|
71
|
+
return new ProtocolError(ErrorCode.METHOD_NOT_FOUND, `Method '${method}' not found`, { method });
|
|
128
72
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
export const RequestSchema: z.ZodObject<
|
|
6
|
+
{
|
|
7
|
+
id: z.ZodString;
|
|
8
|
+
method: z.ZodString;
|
|
9
|
+
params: z.ZodOptional<z.ZodUnknown>;
|
|
10
|
+
},
|
|
11
|
+
z.core.$strip
|
|
12
|
+
> = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
method: z.string(),
|
|
15
|
+
params: z.unknown().optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type Request = z.infer<typeof RequestSchema>;
|
|
19
|
+
|
|
20
|
+
const ErrorSchema: z.ZodObject<
|
|
21
|
+
{ code: z.ZodNumber; message: z.ZodString; data: z.ZodOptional<z.ZodUnknown> },
|
|
22
|
+
z.core.$strip
|
|
23
|
+
> = z.object({
|
|
24
|
+
code: z.number(),
|
|
25
|
+
message: z.string(),
|
|
26
|
+
data: z.unknown().optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const ResponseSchema: z.ZodObject<
|
|
30
|
+
{
|
|
31
|
+
id: z.ZodString;
|
|
32
|
+
result: z.ZodOptional<z.ZodUnknown>;
|
|
33
|
+
error: z.ZodOptional<typeof ErrorSchema>;
|
|
34
|
+
},
|
|
35
|
+
z.core.$strip
|
|
36
|
+
> = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
result: z.unknown().optional(),
|
|
39
|
+
error: ErrorSchema.optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type Response = z.infer<typeof ResponseSchema>;
|
|
43
|
+
|
|
44
|
+
export type SerializedWidget = {
|
|
45
|
+
id: string;
|
|
46
|
+
type: string;
|
|
47
|
+
role: string;
|
|
48
|
+
name: string | null;
|
|
49
|
+
text: string | null;
|
|
50
|
+
sensitive: boolean;
|
|
51
|
+
visible: boolean;
|
|
52
|
+
cssClasses: string[];
|
|
53
|
+
children: SerializedWidget[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type AppInfo = {
|
|
57
|
+
applicationId: string;
|
|
58
|
+
pid: number;
|
|
59
|
+
projectRoot?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const RegisterParamsSchema: z.ZodObject<
|
|
63
|
+
{
|
|
64
|
+
applicationId: z.ZodString;
|
|
65
|
+
pid: z.ZodNumber;
|
|
66
|
+
projectRoot: z.ZodOptional<z.ZodString>;
|
|
67
|
+
},
|
|
68
|
+
z.core.$strip
|
|
69
|
+
> = z.object({
|
|
70
|
+
applicationId: z.string(),
|
|
71
|
+
pid: z.number(),
|
|
72
|
+
projectRoot: z.string().optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const emptyParams: z.ZodObject<Record<string, never>, z.core.$strip> = z.object({});
|
|
76
|
+
export const widgetIdParams: z.ZodObject<{ widgetId: z.ZodString }, z.core.$strip> = z.object({
|
|
77
|
+
widgetId: z.string(),
|
|
78
|
+
});
|
|
79
|
+
export const queryOptionsSchema: z.ZodObject<
|
|
80
|
+
{ name: z.ZodOptional<z.ZodString>; exact: z.ZodOptional<z.ZodBoolean>; timeout: z.ZodOptional<z.ZodNumber> },
|
|
81
|
+
z.core.$strip
|
|
82
|
+
> = z.object({
|
|
83
|
+
name: z.string().optional(),
|
|
84
|
+
exact: z.boolean().optional(),
|
|
85
|
+
timeout: z.number().optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const queryParams: z.ZodObject<
|
|
89
|
+
{
|
|
90
|
+
by: z.ZodEnum<{ role: "role"; text: "text"; name: "name"; labelText: "labelText" }>;
|
|
91
|
+
value: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
|
|
92
|
+
options: z.ZodOptional<typeof queryOptionsSchema>;
|
|
93
|
+
},
|
|
94
|
+
z.core.$strip
|
|
95
|
+
> = z.object({
|
|
96
|
+
by: z.enum(["role", "text", "name", "labelText"]),
|
|
97
|
+
value: z.union([z.string(), z.number()]),
|
|
98
|
+
options: queryOptionsSchema.optional(),
|
|
99
|
+
});
|
|
100
|
+
export const typeParams: z.ZodObject<
|
|
101
|
+
{ widgetId: z.ZodString; text: z.ZodString; clear: z.ZodOptional<z.ZodBoolean> },
|
|
102
|
+
z.core.$strip
|
|
103
|
+
> = z.object({ widgetId: z.string(), text: z.string(), clear: z.boolean().optional() });
|
|
104
|
+
export const fireEventParams: z.ZodObject<
|
|
105
|
+
{ widgetId: z.ZodString; signal: z.ZodString; args: z.ZodOptional<z.ZodArray<z.ZodUnknown>> },
|
|
106
|
+
z.core.$strip
|
|
107
|
+
> = z.object({ widgetId: z.string(), signal: z.string(), args: z.array(z.unknown()).optional() });
|
|
108
|
+
export const screenshotParams: z.ZodObject<
|
|
109
|
+
{ windowId: z.ZodOptional<z.ZodString>; path: z.ZodOptional<z.ZodString> },
|
|
110
|
+
z.core.$strip
|
|
111
|
+
> = z.object({
|
|
112
|
+
windowId: z.string().optional(),
|
|
113
|
+
path: z.string().optional(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const ServerRequestParamsSchemas: {
|
|
117
|
+
"app.getWindows": typeof emptyParams;
|
|
118
|
+
"widget.getTree": typeof emptyParams;
|
|
119
|
+
"widget.query": typeof queryParams;
|
|
120
|
+
"widget.getProps": typeof widgetIdParams;
|
|
121
|
+
"widget.click": typeof widgetIdParams;
|
|
122
|
+
"widget.type": typeof typeParams;
|
|
123
|
+
"widget.fireEvent": typeof fireEventParams;
|
|
124
|
+
"widget.screenshot": typeof screenshotParams;
|
|
125
|
+
} = {
|
|
126
|
+
"app.getWindows": emptyParams,
|
|
127
|
+
"widget.getTree": emptyParams,
|
|
128
|
+
"widget.query": queryParams,
|
|
129
|
+
"widget.getProps": widgetIdParams,
|
|
130
|
+
"widget.click": widgetIdParams,
|
|
131
|
+
"widget.type": typeParams,
|
|
132
|
+
"widget.fireEvent": fireEventParams,
|
|
133
|
+
"widget.screenshot": screenshotParams,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type ServerRequestParams<Method extends keyof typeof ServerRequestParamsSchemas> = z.infer<
|
|
137
|
+
(typeof ServerRequestParamsSchemas)[Method]
|
|
138
|
+
>;
|
|
139
|
+
|
|
140
|
+
export type ParamsSchema<Output> = z.ZodType<Output>;
|
|
141
|
+
|
|
142
|
+
export type ServerInitiatedMethod = keyof typeof ServerRequestParamsSchemas;
|
|
143
|
+
|
|
144
|
+
export type Message = Request | Response;
|
|
145
|
+
|
|
146
|
+
const getRuntimeDir = (): string => process.env.XDG_RUNTIME_DIR ?? tmpdir();
|
|
147
|
+
|
|
148
|
+
export const DEFAULT_SOCKET_PATH: string = join(getRuntimeDir(), "gtkx-mcp.sock");
|
package/src/reference.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { type ApiReference, type ApiSymbol, loadApiReference, resolveGirPath, resolveLibraries } from "@gtkx/codegen";
|
|
4
|
+
import { loadConfig } from "@gtkx/config";
|
|
5
|
+
import { type McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { ErrorCode, McpError, type ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { defineTool, type Tool, textContent, textError } from "./tool.js";
|
|
9
|
+
|
|
10
|
+
export type ReferenceApi = Pick<
|
|
11
|
+
ApiReference,
|
|
12
|
+
"lookup" | "namespaceOverview" | "namespaces" | "overview" | "search" | "symbolNames"
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export type ReferenceProvider = {
|
|
16
|
+
get(): Promise<ReferenceApi>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type WatchedFile = {
|
|
20
|
+
path: string;
|
|
21
|
+
mtimeMs: number;
|
|
22
|
+
size: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type LoadedReference = {
|
|
26
|
+
reference: ApiReference;
|
|
27
|
+
watched: WatchedFile[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const watchFile = (path: string): WatchedFile => {
|
|
31
|
+
try {
|
|
32
|
+
const stats = statSync(path);
|
|
33
|
+
return { path, mtimeMs: stats.mtimeMs, size: stats.size };
|
|
34
|
+
} catch {
|
|
35
|
+
return { path, mtimeMs: -1, size: -1 };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const isFresh = (loaded: LoadedReference): boolean =>
|
|
40
|
+
loaded.watched.every((file) => {
|
|
41
|
+
const current = watchFile(file.path);
|
|
42
|
+
return current.mtimeMs === file.mtimeMs && current.size === file.size;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const loadReference = async (root: string): Promise<LoadedReference> => {
|
|
46
|
+
const { config, configFile } = await loadConfig(root);
|
|
47
|
+
if (config.codegen === false) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`codegen is disabled for the project at ${root}, so there are no generated bindings to document. Remove \`codegen: false\` from gtkx.config.ts to use the API reference.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const girPath = resolveGirPath(config.girPath);
|
|
53
|
+
if (girPath.length === 0) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"No GIR search paths available. Install gobject-introspection (Linux: `sudo dnf install gobject-introspection-devel` or `sudo apt install libgirepository1.0-dev`), or set `girPath` in gtkx.config.ts.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const libraries = resolveLibraries(config.libraries, girPath);
|
|
59
|
+
const reference = loadApiReference({ libraries, girPath, elementProps: config.elementProps ?? {} });
|
|
60
|
+
const watched = [
|
|
61
|
+
...(configFile === undefined ? [] : [watchFile(resolve(root, configFile))]),
|
|
62
|
+
...reference.girFiles.map(watchFile),
|
|
63
|
+
];
|
|
64
|
+
return { reference, watched };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const FRESHNESS_INTERVAL_MS = 2000;
|
|
68
|
+
const FAILURE_RETRY_MS = 5000;
|
|
69
|
+
|
|
70
|
+
type CacheEntry = {
|
|
71
|
+
pending: Promise<LoadedReference>;
|
|
72
|
+
verifiedAt: number;
|
|
73
|
+
failedAt: number | undefined;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const createReferenceProvider = (resolveRoot: () => string): ReferenceProvider => {
|
|
77
|
+
const cache = new Map<string, CacheEntry>();
|
|
78
|
+
const startLoad = (root: string): CacheEntry => {
|
|
79
|
+
const entry: CacheEntry = { pending: loadReference(root), verifiedAt: Date.now(), failedAt: undefined };
|
|
80
|
+
entry.pending.catch(() => {
|
|
81
|
+
entry.failedAt = Date.now();
|
|
82
|
+
});
|
|
83
|
+
cache.set(root, entry);
|
|
84
|
+
return entry;
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
async get(): Promise<ReferenceApi> {
|
|
88
|
+
const root = resolve(resolveRoot());
|
|
89
|
+
let entry = cache.get(root) ?? startLoad(root);
|
|
90
|
+
if (entry.failedAt !== undefined && Date.now() - entry.failedAt >= FAILURE_RETRY_MS) {
|
|
91
|
+
entry = startLoad(root);
|
|
92
|
+
}
|
|
93
|
+
const loaded = await entry.pending;
|
|
94
|
+
if (Date.now() - entry.verifiedAt < FRESHNESS_INTERVAL_MS) return loaded.reference;
|
|
95
|
+
if (isFresh(loaded)) {
|
|
96
|
+
entry.verifiedAt = Date.now();
|
|
97
|
+
return loaded.reference;
|
|
98
|
+
}
|
|
99
|
+
const current = cache.get(root);
|
|
100
|
+
const replacement = current === undefined || current === entry ? startLoad(root) : current;
|
|
101
|
+
return (await replacement.pending).reference;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const SYMBOL_KIND = z.enum([
|
|
107
|
+
"element",
|
|
108
|
+
"class",
|
|
109
|
+
"interface",
|
|
110
|
+
"record",
|
|
111
|
+
"enum",
|
|
112
|
+
"callback",
|
|
113
|
+
"alias",
|
|
114
|
+
"function",
|
|
115
|
+
"constant",
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const SYMBOL_DESCRIPTION =
|
|
119
|
+
"Qualified symbol name (`Gtk.Button`, `Gtk.Orientation`, `GLib.idleAdd`), JSX element name (`GtkButton`), or bare symbol name when unambiguous (`Button`).";
|
|
120
|
+
|
|
121
|
+
const listApiShape = {
|
|
122
|
+
namespace: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Namespace to list (e.g. `Gtk`, `Adw`, `Gio`). Omit for an overview of all namespaces."),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const searchApiShape = {
|
|
129
|
+
query: z.string().describe("Case-insensitive substring of a symbol name, e.g. `headerbar` or `orientation`."),
|
|
130
|
+
namespace: z.string().optional().describe("Restrict matches to one namespace (e.g. `Gtk`)."),
|
|
131
|
+
kind: SYMBOL_KIND.optional().describe("Restrict matches to one symbol kind."),
|
|
132
|
+
limit: z.number().int().min(1).optional().describe("Maximum number of results (default: 20)."),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const getApiDocsShape = {
|
|
136
|
+
symbol: z.string().describe(SYMBOL_DESCRIPTION),
|
|
137
|
+
kind: SYMBOL_KIND.optional().describe("Disambiguate when several kinds share the symbol name."),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const formatCandidates = (candidates: ApiSymbol[]): string =>
|
|
141
|
+
candidates.map((candidate) => `- ${candidate.namespace}.${candidate.name} (${candidate.kind})`).join("\n");
|
|
142
|
+
|
|
143
|
+
const listApiTool = (provider: ReferenceProvider): Tool =>
|
|
144
|
+
defineTool({
|
|
145
|
+
name: "gtkx_list_api",
|
|
146
|
+
title: "List API reference",
|
|
147
|
+
kind: "readOnly",
|
|
148
|
+
description:
|
|
149
|
+
"List the project's generated GTK4 bindings API (`@gtkx/gi` and `@gtkx/jsx`). Without a namespace, returns every namespace with symbol counts; with a namespace, lists all of its symbols grouped by kind.",
|
|
150
|
+
inputSchema: listApiShape,
|
|
151
|
+
handler: async ({ namespace }) => {
|
|
152
|
+
const reference = await provider.get();
|
|
153
|
+
if (namespace === undefined) return textContent(reference.overview());
|
|
154
|
+
const overview = reference.namespaceOverview(namespace);
|
|
155
|
+
if (overview === undefined) {
|
|
156
|
+
const names = reference
|
|
157
|
+
.namespaces()
|
|
158
|
+
.map((summary) => summary.name)
|
|
159
|
+
.join(", ");
|
|
160
|
+
return textError(`Unknown namespace "${namespace}". Available namespaces: ${names}`);
|
|
161
|
+
}
|
|
162
|
+
return textContent(overview);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const searchApiTool = (provider: ReferenceProvider): Tool =>
|
|
167
|
+
defineTool({
|
|
168
|
+
name: "gtkx_search_api",
|
|
169
|
+
title: "Search API reference",
|
|
170
|
+
kind: "readOnly",
|
|
171
|
+
description:
|
|
172
|
+
"Search the project's generated GTK4 bindings API by symbol name. Returns matching symbols with their namespace, kind, and a one-line summary; fetch full pages with `gtkx_get_api_docs`.",
|
|
173
|
+
inputSchema: searchApiShape,
|
|
174
|
+
handler: async ({ query, namespace, kind, limit }) => {
|
|
175
|
+
const reference = await provider.get();
|
|
176
|
+
const results = reference.search({
|
|
177
|
+
query,
|
|
178
|
+
...(namespace === undefined ? {} : { namespace }),
|
|
179
|
+
...(kind === undefined ? {} : { kinds: [kind] }),
|
|
180
|
+
...(limit === undefined ? {} : { limit }),
|
|
181
|
+
});
|
|
182
|
+
if (results.length === 0) {
|
|
183
|
+
return textContent(`No symbols matched "${query}". Try a shorter substring or \`gtkx_list_api\`.`);
|
|
184
|
+
}
|
|
185
|
+
return textContent(JSON.stringify(results, null, 2));
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const getApiDocsTool = (provider: ReferenceProvider): Tool =>
|
|
190
|
+
defineTool({
|
|
191
|
+
name: "gtkx_get_api_docs",
|
|
192
|
+
title: "Get API docs",
|
|
193
|
+
kind: "readOnly",
|
|
194
|
+
description:
|
|
195
|
+
"Get the full reference page for one symbol of the project's generated GTK4 bindings: JSX elements (props, signals, methods) or `@gtkx/gi` classes, interfaces, records, enums, callbacks, aliases, functions, and constants.",
|
|
196
|
+
inputSchema: getApiDocsShape,
|
|
197
|
+
handler: async ({ symbol, kind }) => {
|
|
198
|
+
const reference = await provider.get();
|
|
199
|
+
const result = reference.lookup(symbol, kind);
|
|
200
|
+
if (result.outcome === "notFound") {
|
|
201
|
+
return textError(`No symbol named "${symbol}". Use \`gtkx_search_api\` to find the right name.`);
|
|
202
|
+
}
|
|
203
|
+
if (result.outcome === "ambiguous") {
|
|
204
|
+
return textError(
|
|
205
|
+
`"${symbol}" matches several symbols. Pass a qualified name or a kind:\n${formatCandidates(result.candidates)}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return textContent(result.markdown);
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
export const buildReferenceTools = (provider: ReferenceProvider): Tool[] => [
|
|
213
|
+
listApiTool(provider),
|
|
214
|
+
searchApiTool(provider),
|
|
215
|
+
getApiDocsTool(provider),
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const markdownResource = (uri: URL, text: string): ReadResourceResult => ({
|
|
219
|
+
contents: [{ uri: uri.href, mimeType: "text/markdown", text }],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const variableValue = (value: string | string[] | undefined): string =>
|
|
223
|
+
Array.isArray(value) ? (value[0] ?? "") : (value ?? "");
|
|
224
|
+
|
|
225
|
+
type ResourceServer = Pick<McpServer, "registerResource">;
|
|
226
|
+
|
|
227
|
+
const swallowLoadFailure =
|
|
228
|
+
<T>(fallback: T) =>
|
|
229
|
+
(): T =>
|
|
230
|
+
fallback;
|
|
231
|
+
|
|
232
|
+
const namespaceCompleter =
|
|
233
|
+
(provider: ReferenceProvider) =>
|
|
234
|
+
(value: string): Promise<string[]> =>
|
|
235
|
+
provider
|
|
236
|
+
.get()
|
|
237
|
+
.then((reference) =>
|
|
238
|
+
reference
|
|
239
|
+
.namespaces()
|
|
240
|
+
.map((summary) => summary.name)
|
|
241
|
+
.filter((name) => name.toLowerCase().startsWith(value.toLowerCase())),
|
|
242
|
+
)
|
|
243
|
+
.catch(swallowLoadFailure<string[]>([]));
|
|
244
|
+
|
|
245
|
+
const resourceNotFound = (message: string): McpError => new McpError(ErrorCode.InvalidParams, message);
|
|
246
|
+
|
|
247
|
+
const registerIndexResource = (server: ResourceServer, provider: ReferenceProvider): void => {
|
|
248
|
+
server.registerResource(
|
|
249
|
+
"gtkx-api-reference",
|
|
250
|
+
"gtkx://reference/index",
|
|
251
|
+
{
|
|
252
|
+
title: "GTKX API reference index",
|
|
253
|
+
description: "Namespaces of the project's generated GTK4 bindings, with symbol and JSX element counts.",
|
|
254
|
+
mimeType: "text/markdown",
|
|
255
|
+
},
|
|
256
|
+
async (uri) => markdownResource(uri, (await provider.get()).overview()),
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const registerNamespaceResource = (server: ResourceServer, provider: ReferenceProvider): void => {
|
|
261
|
+
server.registerResource(
|
|
262
|
+
"gtkx-api-namespace",
|
|
263
|
+
new ResourceTemplate("gtkx://reference/{namespace}", {
|
|
264
|
+
list: () =>
|
|
265
|
+
provider
|
|
266
|
+
.get()
|
|
267
|
+
.then((reference) => ({
|
|
268
|
+
resources: reference.namespaces().map((summary) => ({
|
|
269
|
+
uri: `gtkx://reference/${summary.name}`,
|
|
270
|
+
name: `${summary.name} namespace reference`,
|
|
271
|
+
mimeType: "text/markdown",
|
|
272
|
+
})),
|
|
273
|
+
}))
|
|
274
|
+
.catch(swallowLoadFailure({ resources: [] })),
|
|
275
|
+
complete: {
|
|
276
|
+
namespace: namespaceCompleter(provider),
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
{
|
|
280
|
+
title: "GTKX namespace reference",
|
|
281
|
+
description: "All symbols of one namespace of the project's generated GTK4 bindings, grouped by kind.",
|
|
282
|
+
mimeType: "text/markdown",
|
|
283
|
+
},
|
|
284
|
+
async (uri, variables) => {
|
|
285
|
+
const namespace = variableValue(variables.namespace);
|
|
286
|
+
const overview = (await provider.get()).namespaceOverview(namespace);
|
|
287
|
+
if (overview === undefined) throw resourceNotFound(`Unknown namespace "${namespace}"`);
|
|
288
|
+
return markdownResource(uri, overview);
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const registerSymbolResource = (server: ResourceServer, provider: ReferenceProvider): void => {
|
|
294
|
+
server.registerResource(
|
|
295
|
+
"gtkx-api-symbol",
|
|
296
|
+
new ResourceTemplate("gtkx://reference/{namespace}/{symbol}", {
|
|
297
|
+
list: undefined,
|
|
298
|
+
complete: {
|
|
299
|
+
namespace: namespaceCompleter(provider),
|
|
300
|
+
symbol: (value, context) => {
|
|
301
|
+
const namespace = variableValue(context?.arguments?.namespace);
|
|
302
|
+
if (namespace.length === 0) return [];
|
|
303
|
+
return provider
|
|
304
|
+
.get()
|
|
305
|
+
.then((reference) =>
|
|
306
|
+
reference
|
|
307
|
+
.symbolNames(namespace)
|
|
308
|
+
.filter((name) => name.toLowerCase().startsWith(value.toLowerCase())),
|
|
309
|
+
)
|
|
310
|
+
.catch(swallowLoadFailure<string[]>([]));
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
{
|
|
315
|
+
title: "GTKX symbol reference",
|
|
316
|
+
description:
|
|
317
|
+
"Reference page for one symbol of the project's generated GTK4 bindings: a JSX element or a class, interface, record, enum, callback, alias, function, or constant.",
|
|
318
|
+
mimeType: "text/markdown",
|
|
319
|
+
},
|
|
320
|
+
async (uri, variables) => {
|
|
321
|
+
const namespace = variableValue(variables.namespace);
|
|
322
|
+
const symbol = variableValue(variables.symbol);
|
|
323
|
+
const reference = await provider.get();
|
|
324
|
+
const result = reference.lookup(`${namespace}.${symbol}`);
|
|
325
|
+
if (result.outcome === "page") return markdownResource(uri, result.markdown);
|
|
326
|
+
if (result.outcome === "ambiguous") {
|
|
327
|
+
throw resourceNotFound(
|
|
328
|
+
`"${namespace}.${symbol}" matches several symbols:\n${formatCandidates(result.candidates)}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
throw resourceNotFound(`No symbol named "${namespace}.${symbol}"`);
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
export const registerReferenceResources = (server: ResourceServer, provider: ReferenceProvider): void => {
|
|
337
|
+
registerIndexResource(server, provider);
|
|
338
|
+
registerNamespaceResource(server, provider);
|
|
339
|
+
registerSymbolResource(server, provider);
|
|
340
|
+
};
|