servherd 0.0.1 → 1.0.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/CONTRIBUTING.md +250 -0
- package/LICENSE +21 -0
- package/README.md +653 -29
- package/dist/cli/commands/config.d.ts +35 -0
- package/dist/cli/commands/config.js +336 -0
- package/dist/cli/commands/info.d.ts +37 -0
- package/dist/cli/commands/info.js +98 -0
- package/dist/cli/commands/list.d.ts +26 -0
- package/dist/cli/commands/list.js +86 -0
- package/dist/cli/commands/logs.d.ts +46 -0
- package/dist/cli/commands/logs.js +292 -0
- package/dist/cli/commands/mcp.d.ts +5 -0
- package/dist/cli/commands/mcp.js +17 -0
- package/dist/cli/commands/refresh.d.ts +20 -0
- package/dist/cli/commands/refresh.js +139 -0
- package/dist/cli/commands/remove.d.ts +20 -0
- package/dist/cli/commands/remove.js +144 -0
- package/dist/cli/commands/restart.d.ts +25 -0
- package/dist/cli/commands/restart.js +177 -0
- package/dist/cli/commands/start.d.ts +37 -0
- package/dist/cli/commands/start.js +293 -0
- package/dist/cli/commands/stop.d.ts +20 -0
- package/dist/cli/commands/stop.js +108 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +160 -0
- package/dist/cli/output/formatters.d.ts +117 -0
- package/dist/cli/output/formatters.js +454 -0
- package/dist/cli/output/json-formatter.d.ts +22 -0
- package/dist/cli/output/json-formatter.js +40 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +25 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.js +352 -0
- package/dist/mcp/resources/servers.d.ts +14 -0
- package/dist/mcp/resources/servers.js +128 -0
- package/dist/mcp/tools/config.d.ts +33 -0
- package/dist/mcp/tools/config.js +88 -0
- package/dist/mcp/tools/info.d.ts +36 -0
- package/dist/mcp/tools/info.js +65 -0
- package/dist/mcp/tools/list.d.ts +36 -0
- package/dist/mcp/tools/list.js +49 -0
- package/dist/mcp/tools/logs.d.ts +44 -0
- package/dist/mcp/tools/logs.js +55 -0
- package/dist/mcp/tools/refresh.d.ts +33 -0
- package/dist/mcp/tools/refresh.js +54 -0
- package/dist/mcp/tools/remove.d.ts +23 -0
- package/dist/mcp/tools/remove.js +43 -0
- package/dist/mcp/tools/restart.d.ts +23 -0
- package/dist/mcp/tools/restart.js +42 -0
- package/dist/mcp/tools/start.d.ts +38 -0
- package/dist/mcp/tools/start.js +73 -0
- package/dist/mcp/tools/stop.d.ts +23 -0
- package/dist/mcp/tools/stop.js +40 -0
- package/dist/services/config.service.d.ts +80 -0
- package/dist/services/config.service.js +227 -0
- package/dist/services/port.service.d.ts +82 -0
- package/dist/services/port.service.js +151 -0
- package/dist/services/process.service.d.ts +61 -0
- package/dist/services/process.service.js +220 -0
- package/dist/services/registry.service.d.ts +50 -0
- package/dist/services/registry.service.js +157 -0
- package/dist/types/config.d.ts +107 -0
- package/dist/types/config.js +44 -0
- package/dist/types/errors.d.ts +102 -0
- package/dist/types/errors.js +197 -0
- package/dist/types/pm2.d.ts +50 -0
- package/dist/types/pm2.js +4 -0
- package/dist/types/registry.d.ts +230 -0
- package/dist/types/registry.js +33 -0
- package/dist/utils/ci-detector.d.ts +31 -0
- package/dist/utils/ci-detector.js +68 -0
- package/dist/utils/config-drift.d.ts +71 -0
- package/dist/utils/config-drift.js +128 -0
- package/dist/utils/error-handler.d.ts +21 -0
- package/dist/utils/error-handler.js +38 -0
- package/dist/utils/log-follower.d.ts +10 -0
- package/dist/utils/log-follower.js +98 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/names.d.ts +7 -0
- package/dist/utils/names.js +20 -0
- package/dist/utils/template.d.ts +88 -0
- package/dist/utils/template.js +180 -0
- package/dist/utils/time-parser.d.ts +19 -0
- package/dist/utils/time-parser.js +54 -0
- package/docs/ci-cd.md +408 -0
- package/docs/configuration.md +325 -0
- package/docs/mcp-integration.md +411 -0
- package/examples/basic-usage/README.md +187 -0
- package/examples/ci-github-actions/workflow.yml +195 -0
- package/examples/mcp-claude-code/README.md +213 -0
- package/examples/multi-server/README.md +270 -0
- package/examples/storybook/README.md +187 -0
- package/examples/vite-project/README.md +251 -0
- package/package.json +123 -6
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { startToolName, startToolDescription, startToolSchema, handleStartTool, } from "./tools/start.js";
|
|
4
|
+
import { stopToolName, stopToolDescription, stopToolSchema, handleStopTool, } from "./tools/stop.js";
|
|
5
|
+
import { restartToolName, restartToolDescription, restartToolSchema, handleRestartTool, } from "./tools/restart.js";
|
|
6
|
+
import { listToolName, listToolDescription, listToolSchema, handleListTool, } from "./tools/list.js";
|
|
7
|
+
import { infoToolName, infoToolDescription, infoToolSchema, handleInfoTool, } from "./tools/info.js";
|
|
8
|
+
import { logsToolName, logsToolDescription, logsToolSchema, handleLogsTool, } from "./tools/logs.js";
|
|
9
|
+
import { configToolName, configToolDescription, configToolSchema, handleConfigTool, } from "./tools/config.js";
|
|
10
|
+
import { removeToolName, removeToolDescription, removeToolSchema, handleRemoveTool, } from "./tools/remove.js";
|
|
11
|
+
import { refreshToolName, refreshToolDescription, refreshToolSchema, handleRefreshTool, } from "./tools/refresh.js";
|
|
12
|
+
import { listServerResources, readServerResource, } from "./resources/servers.js";
|
|
13
|
+
import { logger } from "../utils/logger.js";
|
|
14
|
+
/**
|
|
15
|
+
* Create and configure an MCP server for servherd
|
|
16
|
+
*/
|
|
17
|
+
export function createMCPServer(options = {}) {
|
|
18
|
+
const name = options.name || "servherd";
|
|
19
|
+
const version = options.version || "0.1.0";
|
|
20
|
+
const server = new McpServer({ name, version }, {
|
|
21
|
+
capabilities: {
|
|
22
|
+
tools: {},
|
|
23
|
+
resources: {},
|
|
24
|
+
},
|
|
25
|
+
instructions: "servherd is a tool for managing development servers. " +
|
|
26
|
+
"Use servherd_start to start a server, servherd_stop to stop it, " +
|
|
27
|
+
"servherd_list to see all servers, and servherd_info to get details about a specific server.",
|
|
28
|
+
});
|
|
29
|
+
// Register tools
|
|
30
|
+
registerTools(server);
|
|
31
|
+
// Register resources
|
|
32
|
+
registerResources(server);
|
|
33
|
+
return server;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Register all tools with the MCP server
|
|
37
|
+
*/
|
|
38
|
+
function registerTools(server) {
|
|
39
|
+
// Start tool
|
|
40
|
+
server.registerTool(startToolName, {
|
|
41
|
+
description: startToolDescription,
|
|
42
|
+
inputSchema: startToolSchema,
|
|
43
|
+
}, async (args) => {
|
|
44
|
+
try {
|
|
45
|
+
const result = await handleStartTool(args);
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: JSON.stringify(result, null, 2),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
logger.error({ error }, "MCP start tool failed");
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Stop tool
|
|
65
|
+
server.registerTool(stopToolName, {
|
|
66
|
+
description: stopToolDescription,
|
|
67
|
+
inputSchema: stopToolSchema,
|
|
68
|
+
}, async (args) => {
|
|
69
|
+
try {
|
|
70
|
+
const result = await handleStopTool(args);
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: JSON.stringify(result, null, 2),
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
logger.error({ error }, "MCP stop tool failed");
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// Restart tool
|
|
90
|
+
server.registerTool(restartToolName, {
|
|
91
|
+
description: restartToolDescription,
|
|
92
|
+
inputSchema: restartToolSchema,
|
|
93
|
+
}, async (args) => {
|
|
94
|
+
try {
|
|
95
|
+
const result = await handleRestartTool(args);
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: JSON.stringify(result, null, 2),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
logger.error({ error }, "MCP restart tool failed");
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
110
|
+
isError: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// List tool
|
|
115
|
+
server.registerTool(listToolName, {
|
|
116
|
+
description: listToolDescription,
|
|
117
|
+
inputSchema: listToolSchema,
|
|
118
|
+
}, async (args) => {
|
|
119
|
+
try {
|
|
120
|
+
const result = await handleListTool(args);
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: JSON.stringify(result, null, 2),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
logger.error({ error }, "MCP list tool failed");
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// Info tool
|
|
140
|
+
server.registerTool(infoToolName, {
|
|
141
|
+
description: infoToolDescription,
|
|
142
|
+
inputSchema: infoToolSchema,
|
|
143
|
+
}, async (args) => {
|
|
144
|
+
try {
|
|
145
|
+
const result = await handleInfoTool(args);
|
|
146
|
+
return {
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: JSON.stringify(result, null, 2),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
157
|
+
logger.error({ error }, "MCP info tool failed");
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
160
|
+
isError: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// Logs tool
|
|
165
|
+
server.registerTool(logsToolName, {
|
|
166
|
+
description: logsToolDescription,
|
|
167
|
+
inputSchema: logsToolSchema,
|
|
168
|
+
}, async (args) => {
|
|
169
|
+
try {
|
|
170
|
+
const result = await handleLogsTool(args);
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: "text",
|
|
175
|
+
text: JSON.stringify(result, null, 2),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
+
logger.error({ error }, "MCP logs tool failed");
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
185
|
+
isError: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// Config tool
|
|
190
|
+
server.registerTool(configToolName, {
|
|
191
|
+
description: configToolDescription,
|
|
192
|
+
inputSchema: configToolSchema,
|
|
193
|
+
}, async (args) => {
|
|
194
|
+
try {
|
|
195
|
+
const result = await handleConfigTool(args);
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: JSON.stringify(result, null, 2),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
logger.error({ error }, "MCP config tool failed");
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
210
|
+
isError: true,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// Remove tool
|
|
215
|
+
server.registerTool(removeToolName, {
|
|
216
|
+
description: removeToolDescription,
|
|
217
|
+
inputSchema: removeToolSchema,
|
|
218
|
+
}, async (args) => {
|
|
219
|
+
try {
|
|
220
|
+
const result = await handleRemoveTool(args);
|
|
221
|
+
return {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: JSON.stringify(result, null, 2),
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
232
|
+
logger.error({ error }, "MCP remove tool failed");
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// Refresh tool
|
|
240
|
+
server.registerTool(refreshToolName, {
|
|
241
|
+
description: refreshToolDescription,
|
|
242
|
+
inputSchema: refreshToolSchema,
|
|
243
|
+
}, async (args) => {
|
|
244
|
+
try {
|
|
245
|
+
const result = await handleRefreshTool(args);
|
|
246
|
+
return {
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: JSON.stringify(result, null, 2),
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
257
|
+
logger.error({ error }, "MCP refresh tool failed");
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Register resources with the MCP server
|
|
267
|
+
*/
|
|
268
|
+
function registerResources(server) {
|
|
269
|
+
// Register server resource template for individual servers
|
|
270
|
+
const serverTemplate = new ResourceTemplate("servherd://servers/{name}", {
|
|
271
|
+
list: async () => {
|
|
272
|
+
const resources = await listServerResources();
|
|
273
|
+
// Filter to only include server resources (not logs)
|
|
274
|
+
const serverResources = resources.filter((r) => !r.uri.endsWith("/logs"));
|
|
275
|
+
return {
|
|
276
|
+
resources: serverResources.map((r) => ({
|
|
277
|
+
uri: r.uri,
|
|
278
|
+
name: r.name,
|
|
279
|
+
description: r.description,
|
|
280
|
+
mimeType: r.mimeType,
|
|
281
|
+
})),
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
server.registerResource("Server Details", serverTemplate, {
|
|
286
|
+
description: "Get details about a specific managed server",
|
|
287
|
+
mimeType: "application/json",
|
|
288
|
+
}, async (uri) => {
|
|
289
|
+
const content = await readServerResource(uri.toString());
|
|
290
|
+
return {
|
|
291
|
+
contents: [
|
|
292
|
+
{
|
|
293
|
+
uri: uri.toString(),
|
|
294
|
+
mimeType: "application/json",
|
|
295
|
+
text: content,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
// Register server logs resource template
|
|
301
|
+
const logsTemplate = new ResourceTemplate("servherd://servers/{name}/logs", {
|
|
302
|
+
list: async () => {
|
|
303
|
+
const resources = await listServerResources();
|
|
304
|
+
// Filter to only include log resources
|
|
305
|
+
const logResources = resources.filter((r) => r.uri.endsWith("/logs"));
|
|
306
|
+
return {
|
|
307
|
+
resources: logResources.map((r) => ({
|
|
308
|
+
uri: r.uri,
|
|
309
|
+
name: r.name,
|
|
310
|
+
description: r.description,
|
|
311
|
+
mimeType: r.mimeType,
|
|
312
|
+
})),
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
server.registerResource("Server Logs", logsTemplate, {
|
|
317
|
+
description: "Get output logs from a managed server",
|
|
318
|
+
mimeType: "text/plain",
|
|
319
|
+
}, async (uri) => {
|
|
320
|
+
const content = await readServerResource(uri.toString());
|
|
321
|
+
return {
|
|
322
|
+
contents: [
|
|
323
|
+
{
|
|
324
|
+
uri: uri.toString(),
|
|
325
|
+
mimeType: "text/plain",
|
|
326
|
+
text: content,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Start the MCP server in stdio mode
|
|
334
|
+
*/
|
|
335
|
+
export async function startStdioServer() {
|
|
336
|
+
const server = createMCPServer();
|
|
337
|
+
const transport = new StdioServerTransport();
|
|
338
|
+
logger.info("Starting MCP server in stdio mode");
|
|
339
|
+
await server.connect(transport);
|
|
340
|
+
// Handle shutdown
|
|
341
|
+
process.on("SIGINT", async () => {
|
|
342
|
+
logger.info("Shutting down MCP server");
|
|
343
|
+
await server.close();
|
|
344
|
+
process.exit(0);
|
|
345
|
+
});
|
|
346
|
+
process.on("SIGTERM", async () => {
|
|
347
|
+
logger.info("Shutting down MCP server");
|
|
348
|
+
await server.close();
|
|
349
|
+
process.exit(0);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
export { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ServerResource {
|
|
2
|
+
uri: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
mimeType?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* List all available server resources
|
|
9
|
+
*/
|
|
10
|
+
export declare function listServerResources(): Promise<ServerResource[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Read a server resource by URI
|
|
13
|
+
*/
|
|
14
|
+
export declare function readServerResource(uri: string): Promise<string>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { RegistryService } from "../../services/registry.service.js";
|
|
2
|
+
import { ProcessService } from "../../services/process.service.js";
|
|
3
|
+
import { executeLogs } from "../../cli/commands/logs.js";
|
|
4
|
+
/**
|
|
5
|
+
* List all available server resources
|
|
6
|
+
*/
|
|
7
|
+
export async function listServerResources() {
|
|
8
|
+
const registryService = new RegistryService();
|
|
9
|
+
try {
|
|
10
|
+
await registryService.load();
|
|
11
|
+
const servers = registryService.listServers();
|
|
12
|
+
const resources = [];
|
|
13
|
+
for (const server of servers) {
|
|
14
|
+
// Add server resource
|
|
15
|
+
resources.push({
|
|
16
|
+
uri: `servherd://servers/${server.name}`,
|
|
17
|
+
name: server.name,
|
|
18
|
+
description: server.description || `Server at ${server.cwd}`,
|
|
19
|
+
mimeType: "application/json",
|
|
20
|
+
});
|
|
21
|
+
// Add logs resource
|
|
22
|
+
resources.push({
|
|
23
|
+
uri: `servherd://servers/${server.name}/logs`,
|
|
24
|
+
name: `${server.name} logs`,
|
|
25
|
+
description: `Output logs for server ${server.name}`,
|
|
26
|
+
mimeType: "text/plain",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return resources;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Return empty list if registry can't be loaded
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read a server resource by URI
|
|
38
|
+
*/
|
|
39
|
+
export async function readServerResource(uri) {
|
|
40
|
+
// Parse URI
|
|
41
|
+
const match = uri.match(/^servherd:\/\/servers\/([^/]+)(\/logs)?$/);
|
|
42
|
+
if (!match) {
|
|
43
|
+
throw new Error(`Invalid resource URI: ${uri}`);
|
|
44
|
+
}
|
|
45
|
+
const serverName = match[1];
|
|
46
|
+
const isLogs = match[2] === "/logs";
|
|
47
|
+
if (isLogs) {
|
|
48
|
+
return readServerLogs(serverName);
|
|
49
|
+
}
|
|
50
|
+
return readServerDetails(serverName);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read server details as JSON
|
|
54
|
+
*/
|
|
55
|
+
async function readServerDetails(name) {
|
|
56
|
+
const registryService = new RegistryService();
|
|
57
|
+
const processService = new ProcessService();
|
|
58
|
+
try {
|
|
59
|
+
await registryService.load();
|
|
60
|
+
await processService.connect();
|
|
61
|
+
const server = registryService.findByName(name);
|
|
62
|
+
if (!server) {
|
|
63
|
+
throw new Error(`Server "${name}" not found`);
|
|
64
|
+
}
|
|
65
|
+
// Get process status
|
|
66
|
+
let status = "unknown";
|
|
67
|
+
let pid;
|
|
68
|
+
let uptime;
|
|
69
|
+
let memory;
|
|
70
|
+
let cpu;
|
|
71
|
+
try {
|
|
72
|
+
const procDesc = await processService.describe(server.pm2Name);
|
|
73
|
+
if (procDesc) {
|
|
74
|
+
const pm2Env = procDesc.pm2_env;
|
|
75
|
+
status = pm2Env.status === "online" ? "online"
|
|
76
|
+
: pm2Env.status === "stopped" || pm2Env.status === "stopping" ? "stopped"
|
|
77
|
+
: pm2Env.status === "errored" ? "errored"
|
|
78
|
+
: "unknown";
|
|
79
|
+
pid = procDesc.pid;
|
|
80
|
+
uptime = pm2Env.pm_uptime;
|
|
81
|
+
if (procDesc.monit) {
|
|
82
|
+
memory = procDesc.monit.memory;
|
|
83
|
+
cpu = procDesc.monit.cpu;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Ignore PM2 errors, just report unknown status
|
|
89
|
+
}
|
|
90
|
+
const serverDetails = {
|
|
91
|
+
name: server.name,
|
|
92
|
+
status,
|
|
93
|
+
port: server.port,
|
|
94
|
+
url: `${server.protocol}://${server.hostname}:${server.port}`,
|
|
95
|
+
cwd: server.cwd,
|
|
96
|
+
command: server.command,
|
|
97
|
+
resolvedCommand: server.resolvedCommand,
|
|
98
|
+
hostname: server.hostname,
|
|
99
|
+
protocol: server.protocol,
|
|
100
|
+
tags: server.tags,
|
|
101
|
+
description: server.description,
|
|
102
|
+
env: server.env,
|
|
103
|
+
createdAt: server.createdAt,
|
|
104
|
+
pm2Name: server.pm2Name,
|
|
105
|
+
pid,
|
|
106
|
+
uptime,
|
|
107
|
+
memory,
|
|
108
|
+
cpu,
|
|
109
|
+
};
|
|
110
|
+
return JSON.stringify(serverDetails, null, 2);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
processService.disconnect();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read server logs
|
|
118
|
+
*/
|
|
119
|
+
async function readServerLogs(name) {
|
|
120
|
+
const result = await executeLogs({
|
|
121
|
+
name,
|
|
122
|
+
lines: 100, // Get more lines for resource reading
|
|
123
|
+
});
|
|
124
|
+
if (!result.logs || result.logs.trim() === "") {
|
|
125
|
+
return "(no logs available)";
|
|
126
|
+
}
|
|
127
|
+
return result.logs;
|
|
128
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const configToolName = "servherd_config";
|
|
3
|
+
export declare const configToolDescription: string;
|
|
4
|
+
export declare const configToolSchema: z.ZodObject<{
|
|
5
|
+
show: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
get: z.ZodOptional<z.ZodString>;
|
|
7
|
+
set: z.ZodOptional<z.ZodString>;
|
|
8
|
+
value: z.ZodOptional<z.ZodString>;
|
|
9
|
+
reset: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
value?: string | undefined;
|
|
12
|
+
set?: string | undefined;
|
|
13
|
+
reset?: boolean | undefined;
|
|
14
|
+
show?: boolean | undefined;
|
|
15
|
+
get?: string | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
value?: string | undefined;
|
|
18
|
+
set?: string | undefined;
|
|
19
|
+
reset?: boolean | undefined;
|
|
20
|
+
show?: boolean | undefined;
|
|
21
|
+
get?: string | undefined;
|
|
22
|
+
}>;
|
|
23
|
+
export type ConfigToolInput = z.infer<typeof configToolSchema>;
|
|
24
|
+
export interface ConfigToolResult {
|
|
25
|
+
action: "show" | "get" | "set" | "reset";
|
|
26
|
+
success: boolean;
|
|
27
|
+
config?: Record<string, unknown>;
|
|
28
|
+
key?: string;
|
|
29
|
+
value?: unknown;
|
|
30
|
+
error?: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function handleConfigTool(input: ConfigToolInput): Promise<ConfigToolResult>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { executeConfig } from "../../cli/commands/config.js";
|
|
3
|
+
export const configToolName = "servherd_config";
|
|
4
|
+
export const configToolDescription = "View or modify servherd global configuration settings. " +
|
|
5
|
+
"Use this tool to check current settings, change configuration values like hostname or port range, or reset to defaults. " +
|
|
6
|
+
"Can show all configuration values, get a specific setting by key, set a new value, or reset all settings to defaults. " +
|
|
7
|
+
"Returns the action performed, success status, relevant configuration data, and a descriptive message. " +
|
|
8
|
+
"Configuration changes persist across sessions and affect all future server operations.";
|
|
9
|
+
export const configToolSchema = z.object({
|
|
10
|
+
show: z.boolean().optional().describe("Set to true to show all configuration values"),
|
|
11
|
+
get: z.string().optional().describe("Get a specific configuration value by key, e.g., 'hostname', 'protocol', or 'portRange.min'"),
|
|
12
|
+
set: z.string().optional().describe("Configuration key to set, e.g., 'hostname' or 'portRange.max'. Requires 'value' parameter"),
|
|
13
|
+
value: z.string().optional().describe("Value to set for the key, e.g., '127.0.0.1' for hostname or '9999' for portRange.max"),
|
|
14
|
+
reset: z.boolean().optional().describe("Set to true to reset all configuration to default values"),
|
|
15
|
+
});
|
|
16
|
+
export async function handleConfigTool(input) {
|
|
17
|
+
// Determine action
|
|
18
|
+
let action = "show";
|
|
19
|
+
if (input.get) {
|
|
20
|
+
action = "get";
|
|
21
|
+
}
|
|
22
|
+
else if (input.set) {
|
|
23
|
+
action = "set";
|
|
24
|
+
}
|
|
25
|
+
else if (input.reset) {
|
|
26
|
+
action = "reset";
|
|
27
|
+
}
|
|
28
|
+
// For reset, we force it since MCP can't prompt
|
|
29
|
+
const result = await executeConfig({
|
|
30
|
+
show: action === "show",
|
|
31
|
+
get: input.get,
|
|
32
|
+
set: input.set,
|
|
33
|
+
value: input.value,
|
|
34
|
+
reset: input.reset,
|
|
35
|
+
force: input.reset ? true : undefined, // Force reset without prompting in MCP context
|
|
36
|
+
});
|
|
37
|
+
// Handle errors
|
|
38
|
+
if (result.error) {
|
|
39
|
+
return {
|
|
40
|
+
action,
|
|
41
|
+
success: false,
|
|
42
|
+
error: result.error,
|
|
43
|
+
message: result.error,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Handle cancelled reset
|
|
47
|
+
if (result.cancelled) {
|
|
48
|
+
return {
|
|
49
|
+
action: "reset",
|
|
50
|
+
success: false,
|
|
51
|
+
message: "Reset was cancelled",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Build response based on action
|
|
55
|
+
switch (action) {
|
|
56
|
+
case "get":
|
|
57
|
+
return {
|
|
58
|
+
action: "get",
|
|
59
|
+
success: true,
|
|
60
|
+
key: result.key,
|
|
61
|
+
value: result.value,
|
|
62
|
+
message: `${result.key} = ${JSON.stringify(result.value)}`,
|
|
63
|
+
};
|
|
64
|
+
case "set":
|
|
65
|
+
return {
|
|
66
|
+
action: "set",
|
|
67
|
+
success: result.updated === true,
|
|
68
|
+
key: result.key,
|
|
69
|
+
value: result.value,
|
|
70
|
+
message: result.updated ? `Set ${result.key} to ${JSON.stringify(result.value)}` : "Failed to set value",
|
|
71
|
+
};
|
|
72
|
+
case "reset":
|
|
73
|
+
return {
|
|
74
|
+
action: "reset",
|
|
75
|
+
success: result.reset === true,
|
|
76
|
+
config: result.config,
|
|
77
|
+
message: "Configuration reset to defaults",
|
|
78
|
+
};
|
|
79
|
+
case "show":
|
|
80
|
+
default:
|
|
81
|
+
return {
|
|
82
|
+
action: "show",
|
|
83
|
+
success: true,
|
|
84
|
+
config: result.config,
|
|
85
|
+
message: `Configuration loaded from ${result.configPath || "defaults"}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const infoToolName = "servherd_info";
|
|
3
|
+
export declare const infoToolDescription: string;
|
|
4
|
+
export declare const infoToolSchema: z.ZodObject<{
|
|
5
|
+
name: z.ZodString;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
name: string;
|
|
8
|
+
}, {
|
|
9
|
+
name: string;
|
|
10
|
+
}>;
|
|
11
|
+
export type InfoToolInput = z.infer<typeof infoToolSchema>;
|
|
12
|
+
export interface InfoToolResult {
|
|
13
|
+
name: string;
|
|
14
|
+
status: string;
|
|
15
|
+
url: string;
|
|
16
|
+
cwd: string;
|
|
17
|
+
command: string;
|
|
18
|
+
resolvedCommand: string;
|
|
19
|
+
port: number;
|
|
20
|
+
hostname: string;
|
|
21
|
+
protocol: string;
|
|
22
|
+
pid?: number;
|
|
23
|
+
uptime?: number;
|
|
24
|
+
uptimeFormatted?: string;
|
|
25
|
+
restarts?: number;
|
|
26
|
+
memory?: number;
|
|
27
|
+
memoryFormatted?: string;
|
|
28
|
+
cpu?: number;
|
|
29
|
+
tags?: string[];
|
|
30
|
+
description?: string;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
pm2Name: string;
|
|
33
|
+
outLogPath?: string;
|
|
34
|
+
errLogPath?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function handleInfoTool(input: InfoToolInput): Promise<InfoToolResult>;
|