@isolo/trinity 1.0.2
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 +180 -0
- package/docs/TOOLS.md +604 -0
- package/package.json +49 -0
- package/src/Config.ts +89 -0
- package/src/Logger.ts +52 -0
- package/src/Server.ts +274 -0
- package/src/Tools.ts +165 -0
- package/src/index.ts +36 -0
package/src/Config.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export class Config {
|
|
4
|
+
private static instance: Config | null = null;
|
|
5
|
+
|
|
6
|
+
private _mode: "stdio" | "http" = "stdio";
|
|
7
|
+
private _targetFolder: string = "";
|
|
8
|
+
private _glob: string = "**/*.ts";
|
|
9
|
+
private _logFile: string = "";
|
|
10
|
+
private _port: number = 3000;
|
|
11
|
+
|
|
12
|
+
private constructor() {}
|
|
13
|
+
|
|
14
|
+
static getInstance(): Config {
|
|
15
|
+
if (!Config.instance) {
|
|
16
|
+
Config.instance = new Config();
|
|
17
|
+
}
|
|
18
|
+
return Config.instance;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
parse(argv: string[]): void {
|
|
22
|
+
// Parse CLI arguments
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
switch (argv[i]) {
|
|
25
|
+
case "--target":
|
|
26
|
+
this._targetFolder = argv[++i];
|
|
27
|
+
break;
|
|
28
|
+
case "--glob":
|
|
29
|
+
this._glob = argv[++i];
|
|
30
|
+
break;
|
|
31
|
+
case "--port":
|
|
32
|
+
this._port = parseInt(argv[++i], 10);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Env fallbacks (CLI takes priority)
|
|
38
|
+
const envMode = process.env.MCP_MODE;
|
|
39
|
+
if (envMode === "http" || envMode === "stdio") {
|
|
40
|
+
this._mode = envMode;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!this._glob || this._glob === "**/*.ts") {
|
|
44
|
+
this._glob = process.env.MCP_GLOB || "**/*.ts";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.env.LOG_FILE) {
|
|
48
|
+
this._logFile = process.env.LOG_FILE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!this._targetFolder) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Missing required argument: --target <path>. Specify the folder containing tool files."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Resolve to absolute path
|
|
58
|
+
this._targetFolder = path.resolve(this._targetFolder);
|
|
59
|
+
|
|
60
|
+
// Default log file relative to target folder
|
|
61
|
+
if (!this._logFile) {
|
|
62
|
+
this._logFile = path.join(this._targetFolder, "debug.log");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get mode(): "stdio" | "http" {
|
|
67
|
+
return this._mode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get targetFolder(): string {
|
|
71
|
+
return this._targetFolder;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get glob(): string {
|
|
75
|
+
return this._glob;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get logFile(): string {
|
|
79
|
+
return this._logFile;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get logDest(): "stdout" | "file" {
|
|
83
|
+
return this._mode === "http" ? "stdout" : "file";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get port(): number {
|
|
87
|
+
return this._port;
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/Logger.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { Config } from "./Config.js";
|
|
3
|
+
import safeJsonStringify from "json-stringify-safe";
|
|
4
|
+
|
|
5
|
+
type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG";
|
|
6
|
+
|
|
7
|
+
export class Logger {
|
|
8
|
+
private name: string;
|
|
9
|
+
private config: Config;
|
|
10
|
+
|
|
11
|
+
constructor(requester: string | { name: string }) {
|
|
12
|
+
this.name = typeof requester === "string" ? requester : requester.name;
|
|
13
|
+
this.config = Config.getInstance();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
debug(msg: string, meta?: any): void {
|
|
17
|
+
this.log("DEBUG", msg, meta);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
info(msg: string, meta?: any): void {
|
|
21
|
+
this.log("INFO", msg, meta);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
warn(msg: string, meta?: any): void {
|
|
25
|
+
this.log("WARN", msg, meta);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
error(msg: string, meta?: any): void {
|
|
29
|
+
this.log("ERROR", msg, meta);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private format(level: LogLevel, msg: string, meta: any = {}): string {
|
|
33
|
+
const timestamp = new Date().toISOString();
|
|
34
|
+
const logEntry = {
|
|
35
|
+
timestamp,
|
|
36
|
+
level,
|
|
37
|
+
name: this.name,
|
|
38
|
+
msg,
|
|
39
|
+
...(Object.keys(meta).length > 0 && { meta }),
|
|
40
|
+
};
|
|
41
|
+
return safeJsonStringify(logEntry);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private log(level: LogLevel, msg: string, meta?: any): void {
|
|
45
|
+
const formatted = this.format(level, msg, meta);
|
|
46
|
+
if (this.config.logDest === "stdout") {
|
|
47
|
+
console.error(formatted);
|
|
48
|
+
} else {
|
|
49
|
+
fs.appendFileSync(this.config.logFile, formatted + "\n");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/Server.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Server as SDKServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
ListResourcesRequestSchema,
|
|
7
|
+
ReadResourceRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { Config } from "./Config.js";
|
|
13
|
+
import { Logger } from "./Logger.js";
|
|
14
|
+
import { Tools } from "./Tools.js";
|
|
15
|
+
import safeJsonStringify from "json-stringify-safe";
|
|
16
|
+
|
|
17
|
+
export class Server {
|
|
18
|
+
private static instance: Server | null = null;
|
|
19
|
+
|
|
20
|
+
static getInstance(): Server {
|
|
21
|
+
if (!Server.instance) {
|
|
22
|
+
Server.instance = new Server();
|
|
23
|
+
}
|
|
24
|
+
return Server.instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private server = new SDKServer(
|
|
28
|
+
{ name: "trinity", version: "0.1.0" },
|
|
29
|
+
{
|
|
30
|
+
capabilities: {
|
|
31
|
+
tools: { listChanged: true },
|
|
32
|
+
resources: { listChanged: true },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
private constructor(
|
|
38
|
+
private logger = new Logger(Server),
|
|
39
|
+
private config = Config.getInstance(),
|
|
40
|
+
private tools = Tools.getInstance(),
|
|
41
|
+
) {
|
|
42
|
+
this.registerHandlers();
|
|
43
|
+
this.tools.onChange(() => this.onToolsChanged());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async start(): Promise<void> {
|
|
47
|
+
this.logger.info(`Starting server in`, {
|
|
48
|
+
mode: this.config.mode,
|
|
49
|
+
targetFolder: this.config.targetFolder,
|
|
50
|
+
glob: this.config.glob,
|
|
51
|
+
port: this.config.port,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.tools.startWatching();
|
|
55
|
+
|
|
56
|
+
const transport = await this.createTransport();
|
|
57
|
+
await this.server.connect(transport);
|
|
58
|
+
|
|
59
|
+
await this.server.sendToolListChanged();
|
|
60
|
+
|
|
61
|
+
await this.server.sendResourceListChanged();
|
|
62
|
+
|
|
63
|
+
this.logger.info("Server started successfully");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async stop(): Promise<void> {
|
|
67
|
+
this.logger.info("Stopping server...");
|
|
68
|
+
this.tools.stopWatching();
|
|
69
|
+
await this.server.close();
|
|
70
|
+
this.logger.info("Server stopped");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private registerHandlers(): void {
|
|
74
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
75
|
+
this.handleListTools(),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
|
79
|
+
this.handleCallTool(request),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () =>
|
|
83
|
+
this.handleListResources(),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) =>
|
|
87
|
+
this.handleReadResource(request),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async handleListTools() {
|
|
92
|
+
this.logger.debug("Handling list_tools request");
|
|
93
|
+
|
|
94
|
+
const toolDefs = await this.tools.loadTools();
|
|
95
|
+
|
|
96
|
+
const tools = toolDefs.map((tool) => {
|
|
97
|
+
// Convert Zod schema to JSON Schema for the MCP protocol
|
|
98
|
+
const jsonSchema = tool.inputSchema.toJSONSchema();
|
|
99
|
+
|
|
100
|
+
this.logger.debug(`Generated JSON schema for tool`, {
|
|
101
|
+
name: tool.name,
|
|
102
|
+
jsonSchema,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
name: tool.name,
|
|
107
|
+
description: tool.description,
|
|
108
|
+
inputSchema: jsonSchema as {
|
|
109
|
+
type: "object";
|
|
110
|
+
properties?: Record<string, unknown>;
|
|
111
|
+
required?: string[];
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.logger.debug(`Returning ${tools.length} tools`);
|
|
117
|
+
return { tools };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async handleCallTool(request: {
|
|
121
|
+
params: { name: string; arguments?: Record<string, unknown> };
|
|
122
|
+
}) {
|
|
123
|
+
const { name, arguments: args } = request.params;
|
|
124
|
+
this.logger.info(`Calling tool: ${name}`);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const tool = await this.tools.loadTool(name);
|
|
128
|
+
const raw = await tool.execute(args || {});
|
|
129
|
+
this.logger.debug(`Tool ${name} executed successfully`, { raw });
|
|
130
|
+
|
|
131
|
+
return this.formatToolResult(raw);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this.logger.error(`Tool ${name} execution failed`, err as Error);
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text" as const,
|
|
138
|
+
text: `Error: ${(err as Error).message}`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
isError: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private formatToolResult(raw: any) {
|
|
147
|
+
// Handle arrays: each item becomes a content item
|
|
148
|
+
if (Array.isArray(raw)) {
|
|
149
|
+
return { content: raw.map((item) => this.formatContentItem(item)) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Handle objects with MCP content types (image, audio, resource_link, resource)
|
|
153
|
+
if (raw && typeof raw === "object") {
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: safeJsonStringify(raw, null, 2) }],
|
|
156
|
+
structuredContent: raw,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { content: [{ type: "text", text: String(raw) }] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private formatContentItem(item: any): any {
|
|
164
|
+
// If it's already an MCP content item, return as-is
|
|
165
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
166
|
+
return item;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Otherwise, convert to text
|
|
170
|
+
const text =
|
|
171
|
+
typeof item === "string"
|
|
172
|
+
? item
|
|
173
|
+
: item === null || item === undefined
|
|
174
|
+
? String(item)
|
|
175
|
+
: typeof item === "object"
|
|
176
|
+
? JSON.stringify(item, null, 2)
|
|
177
|
+
: String(item);
|
|
178
|
+
|
|
179
|
+
return { type: "text", text };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async createTransport(): Promise<Transport> {
|
|
183
|
+
if (this.config.mode === "stdio") {
|
|
184
|
+
this.logger.info("Using stdio transport");
|
|
185
|
+
return new StdioServerTransport();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.logger.info(`Starting HTTP server on port ${this.config.port}`);
|
|
189
|
+
return this.createHttpTransport();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async createHttpTransport(): Promise<Transport> {
|
|
193
|
+
// Use WebStandardStreamableHTTPServerTransport — works natively with Bun's fetch API
|
|
194
|
+
const { WebStandardStreamableHTTPServerTransport } =
|
|
195
|
+
await import("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
|
|
196
|
+
|
|
197
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
198
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
Bun.serve({
|
|
202
|
+
port: this.config.port,
|
|
203
|
+
fetch: async (req: Request): Promise<Response> => {
|
|
204
|
+
const url = new URL(req.url);
|
|
205
|
+
|
|
206
|
+
if (url.pathname === "/mcp") {
|
|
207
|
+
return await transport.handleRequest(req);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (url.pathname === "/health") {
|
|
211
|
+
return new Response(JSON.stringify({ status: "ok" }), {
|
|
212
|
+
headers: { "Content-Type": "application/json" },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return new Response("Not Found", { status: 404 });
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
this.logger.info(`HTTP server listening on port ${this.config.port}`);
|
|
221
|
+
return transport;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async handleListResources() {
|
|
225
|
+
this.logger.debug("Handling list_resources request");
|
|
226
|
+
|
|
227
|
+
const resources = [
|
|
228
|
+
{
|
|
229
|
+
uri: "trinity://docs/tools",
|
|
230
|
+
name: "How to implement/define Tools",
|
|
231
|
+
description:
|
|
232
|
+
"Comprehensive guide for creating and managing tools with Trinity MCP, including schemas, hot reload, and best practices",
|
|
233
|
+
mimeType: "text/markdown",
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
this.logger.debug(`Returning ${resources.length} resources`);
|
|
238
|
+
return { resources };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async handleReadResource(request: { params: { uri: string } }) {
|
|
242
|
+
const { uri } = request.params;
|
|
243
|
+
this.logger.info(`Reading resource: ${uri}`);
|
|
244
|
+
|
|
245
|
+
if (uri === "trinity://docs/tools") {
|
|
246
|
+
try {
|
|
247
|
+
const docsPath = path.resolve(import.meta.dir, "../docs/TOOLS.md");
|
|
248
|
+
const content = fs.readFileSync(docsPath, "utf-8");
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
contents: [
|
|
252
|
+
{
|
|
253
|
+
uri,
|
|
254
|
+
mimeType: "text/markdown",
|
|
255
|
+
text: content,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
} catch (err) {
|
|
260
|
+
this.logger.error(`Failed to read resource ${uri}`, err as Error);
|
|
261
|
+
throw new Error(`Failed to read resource: ${(err as Error).message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private onToolsChanged(): void {
|
|
269
|
+
this.logger.info("Tools changed, notifying clients");
|
|
270
|
+
this.server.sendToolListChanged().catch((err: Error) => {
|
|
271
|
+
this.logger.error("Failed to send tool list changed notification", err);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
package/src/Tools.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Glob } from "bun";
|
|
4
|
+
import { Config } from "./Config.js";
|
|
5
|
+
import { Logger } from "./Logger.js";
|
|
6
|
+
import { ZodType } from "zod";
|
|
7
|
+
|
|
8
|
+
export interface ToolDefinition {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
inputSchema: ZodType;
|
|
12
|
+
execute: (args: Record<string, unknown>) => Promise<any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Tools {
|
|
16
|
+
private static instance: Tools | null = null;
|
|
17
|
+
|
|
18
|
+
static getInstance(): Tools {
|
|
19
|
+
if (!Tools.instance) {
|
|
20
|
+
Tools.instance = new Tools();
|
|
21
|
+
}
|
|
22
|
+
return Tools.instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private watcher: fs.FSWatcher | null = null;
|
|
26
|
+
private onChangeCallback: (() => void) | null = null;
|
|
27
|
+
|
|
28
|
+
private constructor(
|
|
29
|
+
private logger = new Logger(Tools),
|
|
30
|
+
private config = Config.getInstance()
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
async loadTools(): Promise<ToolDefinition[]> {
|
|
34
|
+
const files = await this.resolveToolFiles();
|
|
35
|
+
this.logger.debug(`Resolved ${files.length} tool files`);
|
|
36
|
+
|
|
37
|
+
const tools: ToolDefinition[] = [];
|
|
38
|
+
|
|
39
|
+
for (const filePath of files) {
|
|
40
|
+
try {
|
|
41
|
+
const tool = await this.loadToolFromFile(filePath);
|
|
42
|
+
tools.push(tool);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
this.logger.error(`Failed to load tool from ${filePath}`, err as Error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return tools;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async loadTool(name: string): Promise<ToolDefinition> {
|
|
52
|
+
const files = await this.resolveToolFiles();
|
|
53
|
+
const filePath = files.find(
|
|
54
|
+
(f) => path.basename(f, path.extname(f)) === name
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!filePath) {
|
|
58
|
+
throw new Error(`Tool not found: ${name}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this.loadToolFromFile(filePath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onChange(callback: () => void): void {
|
|
65
|
+
this.onChangeCallback = callback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
startWatching(): void {
|
|
69
|
+
const targetFolder = this.config.targetFolder;
|
|
70
|
+
this.logger.info(`Watching for changes in ${targetFolder}`);
|
|
71
|
+
|
|
72
|
+
this.watcher = fs.watch(
|
|
73
|
+
targetFolder,
|
|
74
|
+
{ recursive: true },
|
|
75
|
+
(event: string, filename: string | null) => {
|
|
76
|
+
if (filename) {
|
|
77
|
+
this.onFileChange(event, filename);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
this.watcher.on("error", (err: Error) => {
|
|
83
|
+
this.logger.error("Watcher error", err);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
stopWatching(): void {
|
|
88
|
+
if (this.watcher) {
|
|
89
|
+
this.watcher.close();
|
|
90
|
+
this.watcher = null;
|
|
91
|
+
this.logger.info("Stopped watching for changes");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async resolveToolFiles(): Promise<string[]> {
|
|
96
|
+
const glob = new Glob(this.config.glob);
|
|
97
|
+
const targetFolder = this.config.targetFolder;
|
|
98
|
+
const files: string[] = [];
|
|
99
|
+
|
|
100
|
+
for await (const file of glob.scan({ cwd: targetFolder, absolute: true })) {
|
|
101
|
+
files.push(file);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return files;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async loadToolFromFile(filePath: string): Promise<ToolDefinition> {
|
|
108
|
+
const mod = await this.dynamicImport(filePath);
|
|
109
|
+
const name = path.basename(filePath, path.extname(filePath));
|
|
110
|
+
|
|
111
|
+
if (!mod.description || typeof mod.description !== "string") {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Tool file ${filePath} must export a 'description' string`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!mod.inputSchema || typeof mod.inputSchema !== "object") {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Tool file ${filePath} must export an 'inputSchema' object`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!mod.default || typeof mod.default !== "function") {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Tool file ${filePath} must have a default export function (execute)`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
name,
|
|
131
|
+
description: this.buildDescription(mod.description, filePath),
|
|
132
|
+
inputSchema: mod.inputSchema as ZodType,
|
|
133
|
+
execute: mod.default as ToolDefinition["execute"],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async dynamicImport(filePath: string): Promise<Record<string, unknown>> {
|
|
138
|
+
// Cache-busting with timestamp to always get fresh module
|
|
139
|
+
return import(`${filePath}?t=${Date.now()}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private buildDescription(description: string, filePath: string): string {
|
|
143
|
+
return [
|
|
144
|
+
description,
|
|
145
|
+
"",
|
|
146
|
+
"---",
|
|
147
|
+
`Source: ${filePath}`,
|
|
148
|
+
"Note: This tool is dynamically loaded. Changes to this file will immediately affect this tool's behavior. You can read and modify this file to change the tool.",
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private onFileChange(event: string, filename: string): void {
|
|
153
|
+
// Check if the changed file matches our glob pattern
|
|
154
|
+
const glob = new Glob(this.config.glob);
|
|
155
|
+
if (!glob.match(filename)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.logger.info(`File ${event}: ${filename}`);
|
|
160
|
+
|
|
161
|
+
if (this.onChangeCallback) {
|
|
162
|
+
this.onChangeCallback();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Config } from "./Config.js";
|
|
4
|
+
import { Server } from "./Server.js";
|
|
5
|
+
import { Logger } from "./Logger.js";
|
|
6
|
+
|
|
7
|
+
const logger = new Logger("main");
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
Config.getInstance().parse(process.argv.slice(2));
|
|
11
|
+
} catch (err) {
|
|
12
|
+
console.error((err as Error).message);
|
|
13
|
+
console.error("Usage: bun --hot src/index.ts --target <path> [--glob <pattern>] [--port <number>]");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const server = Server.getInstance();
|
|
18
|
+
|
|
19
|
+
process.on("SIGINT", async () => {
|
|
20
|
+
logger.info("Received SIGINT, shutting down...");
|
|
21
|
+
await server.stop();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
process.on("SIGTERM", async () => {
|
|
26
|
+
logger.info("Received SIGTERM, shutting down...");
|
|
27
|
+
await server.stop();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await server.start();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.error("Failed to start server", err as Error);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|