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.
Files changed (95) hide show
  1. package/CONTRIBUTING.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +653 -29
  4. package/dist/cli/commands/config.d.ts +35 -0
  5. package/dist/cli/commands/config.js +336 -0
  6. package/dist/cli/commands/info.d.ts +37 -0
  7. package/dist/cli/commands/info.js +98 -0
  8. package/dist/cli/commands/list.d.ts +26 -0
  9. package/dist/cli/commands/list.js +86 -0
  10. package/dist/cli/commands/logs.d.ts +46 -0
  11. package/dist/cli/commands/logs.js +292 -0
  12. package/dist/cli/commands/mcp.d.ts +5 -0
  13. package/dist/cli/commands/mcp.js +17 -0
  14. package/dist/cli/commands/refresh.d.ts +20 -0
  15. package/dist/cli/commands/refresh.js +139 -0
  16. package/dist/cli/commands/remove.d.ts +20 -0
  17. package/dist/cli/commands/remove.js +144 -0
  18. package/dist/cli/commands/restart.d.ts +25 -0
  19. package/dist/cli/commands/restart.js +177 -0
  20. package/dist/cli/commands/start.d.ts +37 -0
  21. package/dist/cli/commands/start.js +293 -0
  22. package/dist/cli/commands/stop.d.ts +20 -0
  23. package/dist/cli/commands/stop.js +108 -0
  24. package/dist/cli/index.d.ts +9 -0
  25. package/dist/cli/index.js +160 -0
  26. package/dist/cli/output/formatters.d.ts +117 -0
  27. package/dist/cli/output/formatters.js +454 -0
  28. package/dist/cli/output/json-formatter.d.ts +22 -0
  29. package/dist/cli/output/json-formatter.js +40 -0
  30. package/dist/index.d.ts +15 -0
  31. package/dist/index.js +25 -0
  32. package/dist/mcp/index.d.ts +14 -0
  33. package/dist/mcp/index.js +352 -0
  34. package/dist/mcp/resources/servers.d.ts +14 -0
  35. package/dist/mcp/resources/servers.js +128 -0
  36. package/dist/mcp/tools/config.d.ts +33 -0
  37. package/dist/mcp/tools/config.js +88 -0
  38. package/dist/mcp/tools/info.d.ts +36 -0
  39. package/dist/mcp/tools/info.js +65 -0
  40. package/dist/mcp/tools/list.d.ts +36 -0
  41. package/dist/mcp/tools/list.js +49 -0
  42. package/dist/mcp/tools/logs.d.ts +44 -0
  43. package/dist/mcp/tools/logs.js +55 -0
  44. package/dist/mcp/tools/refresh.d.ts +33 -0
  45. package/dist/mcp/tools/refresh.js +54 -0
  46. package/dist/mcp/tools/remove.d.ts +23 -0
  47. package/dist/mcp/tools/remove.js +43 -0
  48. package/dist/mcp/tools/restart.d.ts +23 -0
  49. package/dist/mcp/tools/restart.js +42 -0
  50. package/dist/mcp/tools/start.d.ts +38 -0
  51. package/dist/mcp/tools/start.js +73 -0
  52. package/dist/mcp/tools/stop.d.ts +23 -0
  53. package/dist/mcp/tools/stop.js +40 -0
  54. package/dist/services/config.service.d.ts +80 -0
  55. package/dist/services/config.service.js +227 -0
  56. package/dist/services/port.service.d.ts +82 -0
  57. package/dist/services/port.service.js +151 -0
  58. package/dist/services/process.service.d.ts +61 -0
  59. package/dist/services/process.service.js +220 -0
  60. package/dist/services/registry.service.d.ts +50 -0
  61. package/dist/services/registry.service.js +157 -0
  62. package/dist/types/config.d.ts +107 -0
  63. package/dist/types/config.js +44 -0
  64. package/dist/types/errors.d.ts +102 -0
  65. package/dist/types/errors.js +197 -0
  66. package/dist/types/pm2.d.ts +50 -0
  67. package/dist/types/pm2.js +4 -0
  68. package/dist/types/registry.d.ts +230 -0
  69. package/dist/types/registry.js +33 -0
  70. package/dist/utils/ci-detector.d.ts +31 -0
  71. package/dist/utils/ci-detector.js +68 -0
  72. package/dist/utils/config-drift.d.ts +71 -0
  73. package/dist/utils/config-drift.js +128 -0
  74. package/dist/utils/error-handler.d.ts +21 -0
  75. package/dist/utils/error-handler.js +38 -0
  76. package/dist/utils/log-follower.d.ts +10 -0
  77. package/dist/utils/log-follower.js +98 -0
  78. package/dist/utils/logger.d.ts +11 -0
  79. package/dist/utils/logger.js +24 -0
  80. package/dist/utils/names.d.ts +7 -0
  81. package/dist/utils/names.js +20 -0
  82. package/dist/utils/template.d.ts +88 -0
  83. package/dist/utils/template.js +180 -0
  84. package/dist/utils/time-parser.d.ts +19 -0
  85. package/dist/utils/time-parser.js +54 -0
  86. package/docs/ci-cd.md +408 -0
  87. package/docs/configuration.md +325 -0
  88. package/docs/mcp-integration.md +411 -0
  89. package/examples/basic-usage/README.md +187 -0
  90. package/examples/ci-github-actions/workflow.yml +195 -0
  91. package/examples/mcp-claude-code/README.md +213 -0
  92. package/examples/multi-server/README.md +270 -0
  93. package/examples/storybook/README.md +187 -0
  94. package/examples/vite-project/README.md +251 -0
  95. package/package.json +123 -6
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ import { executeInfo } from "../../cli/commands/info.js";
3
+ export const infoToolName = "servherd_info";
4
+ export const infoToolDescription = "Get detailed information about a specific server. " +
5
+ "Use this tool when you need comprehensive details about a server's configuration, health, or resource usage. " +
6
+ "Returns status, port, URL, working directory, command, process ID, uptime (raw and formatted), restart count, memory usage (raw bytes and formatted), CPU percentage, tags, description, and log file paths. " +
7
+ "This provides more detail than servherd_list which gives a summary of all servers.";
8
+ export const infoToolSchema = z.object({
9
+ name: z.string().describe("Name of the server to get info for, e.g., 'frontend-dev' or 'brave-tiger'"),
10
+ });
11
+ function formatUptime(ms) {
12
+ if (ms === undefined) {
13
+ return undefined;
14
+ }
15
+ const seconds = Math.floor((Date.now() - ms) / 1000);
16
+ const minutes = Math.floor(seconds / 60);
17
+ const hours = Math.floor(minutes / 60);
18
+ const days = Math.floor(hours / 24);
19
+ if (days > 0) {
20
+ return `${days}d ${hours % 24}h`;
21
+ }
22
+ if (hours > 0) {
23
+ return `${hours}h ${minutes % 60}m`;
24
+ }
25
+ if (minutes > 0) {
26
+ return `${minutes}m ${seconds % 60}s`;
27
+ }
28
+ return `${seconds}s`;
29
+ }
30
+ function formatMemory(bytes) {
31
+ if (bytes === undefined) {
32
+ return undefined;
33
+ }
34
+ const mb = bytes / 1024 / 1024;
35
+ return `${mb.toFixed(1)} MB`;
36
+ }
37
+ export async function handleInfoTool(input) {
38
+ const result = await executeInfo({
39
+ name: input.name,
40
+ });
41
+ return {
42
+ name: result.name,
43
+ status: result.status,
44
+ url: result.url,
45
+ cwd: result.cwd,
46
+ command: result.command,
47
+ resolvedCommand: result.resolvedCommand,
48
+ port: result.port,
49
+ hostname: result.hostname,
50
+ protocol: result.protocol,
51
+ pid: result.pid,
52
+ uptime: result.uptime,
53
+ uptimeFormatted: formatUptime(result.uptime),
54
+ restarts: result.restarts,
55
+ memory: result.memory,
56
+ memoryFormatted: formatMemory(result.memory),
57
+ cpu: result.cpu,
58
+ tags: result.tags,
59
+ description: result.description,
60
+ createdAt: result.createdAt,
61
+ pm2Name: result.pm2Name,
62
+ outLogPath: result.outLogPath,
63
+ errLogPath: result.errLogPath,
64
+ };
65
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ export declare const listToolName = "servherd_list";
3
+ export declare const listToolDescription: string;
4
+ export declare const listToolSchema: z.ZodObject<{
5
+ running: z.ZodOptional<z.ZodBoolean>;
6
+ tag: z.ZodOptional<z.ZodString>;
7
+ cwd: z.ZodOptional<z.ZodString>;
8
+ cmd: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ cwd?: string | undefined;
11
+ tag?: string | undefined;
12
+ cmd?: string | undefined;
13
+ running?: boolean | undefined;
14
+ }, {
15
+ cwd?: string | undefined;
16
+ tag?: string | undefined;
17
+ cmd?: string | undefined;
18
+ running?: boolean | undefined;
19
+ }>;
20
+ export type ListToolInput = z.infer<typeof listToolSchema>;
21
+ export interface ServerInfo {
22
+ name: string;
23
+ status: string;
24
+ port: number;
25
+ url: string;
26
+ cwd: string;
27
+ command: string;
28
+ tags?: string[];
29
+ hasDrift?: boolean;
30
+ }
31
+ export interface ListToolResult {
32
+ servers: ServerInfo[];
33
+ count: number;
34
+ summary: string;
35
+ }
36
+ export declare function handleListTool(input: ListToolInput): Promise<ListToolResult>;
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { executeList } from "../../cli/commands/list.js";
3
+ export const listToolName = "servherd_list";
4
+ export const listToolDescription = "List all managed development servers with their current status. " +
5
+ "Use this tool to get an overview of all servers, check which servers are running, or find servers by tag, location, or command. " +
6
+ "Can filter to show only running servers, servers with a specific tag, servers in a particular directory, or servers matching a command pattern (e.g., '*storybook*'). " +
7
+ "Returns an array of server objects with name, status (online/stopped/errored), port, URL, working directory, command, and tags. " +
8
+ "Also includes a count and summary message.";
9
+ export const listToolSchema = z.object({
10
+ running: z.boolean().optional().describe("Set to true to only show servers that are currently running"),
11
+ tag: z.string().optional().describe("Filter by tag, e.g., 'frontend' or 'api'"),
12
+ cwd: z.string().optional().describe("Filter by working directory, e.g., '/home/user/projects/my-app'"),
13
+ cmd: z.string().optional().describe("Filter by command pattern using glob syntax, e.g., '*storybook*' or '*vite*'"),
14
+ });
15
+ export async function handleListTool(input) {
16
+ const result = await executeList({
17
+ running: input.running,
18
+ tag: input.tag,
19
+ cwd: input.cwd,
20
+ cmd: input.cmd,
21
+ });
22
+ const servers = result.servers.map((item) => ({
23
+ name: item.server.name,
24
+ status: item.status,
25
+ port: item.server.port,
26
+ url: `${item.server.protocol}://${item.server.hostname}:${item.server.port}`,
27
+ cwd: item.server.cwd,
28
+ command: item.server.command,
29
+ tags: item.server.tags,
30
+ hasDrift: item.hasDrift,
31
+ }));
32
+ const runningCount = servers.filter((s) => s.status === "online").length;
33
+ const driftCount = servers.filter((s) => s.hasDrift).length;
34
+ let summary;
35
+ if (servers.length === 0) {
36
+ summary = "No servers found";
37
+ }
38
+ else {
39
+ summary = `${servers.length} server${servers.length !== 1 ? "s" : ""} (${runningCount} running)`;
40
+ if (driftCount > 0) {
41
+ summary += `. ${driftCount} server${driftCount !== 1 ? "s have" : " has"} config drift - use servherd_restart to apply new config`;
42
+ }
43
+ }
44
+ return {
45
+ servers,
46
+ count: servers.length,
47
+ summary,
48
+ };
49
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ export declare const logsToolName = "servherd_logs";
3
+ export declare const logsToolDescription: string;
4
+ export declare const logsToolSchema: z.ZodObject<{
5
+ name: z.ZodOptional<z.ZodString>;
6
+ lines: z.ZodOptional<z.ZodNumber>;
7
+ error: z.ZodOptional<z.ZodBoolean>;
8
+ since: z.ZodOptional<z.ZodString>;
9
+ head: z.ZodOptional<z.ZodNumber>;
10
+ flush: z.ZodOptional<z.ZodBoolean>;
11
+ all: z.ZodOptional<z.ZodBoolean>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ error?: boolean | undefined;
14
+ flush?: boolean | undefined;
15
+ name?: string | undefined;
16
+ all?: boolean | undefined;
17
+ lines?: number | undefined;
18
+ head?: number | undefined;
19
+ since?: string | undefined;
20
+ }, {
21
+ error?: boolean | undefined;
22
+ flush?: boolean | undefined;
23
+ name?: string | undefined;
24
+ all?: boolean | undefined;
25
+ lines?: number | undefined;
26
+ head?: number | undefined;
27
+ since?: string | undefined;
28
+ }>;
29
+ export type LogsToolInput = z.infer<typeof logsToolSchema>;
30
+ export interface LogsToolResult {
31
+ name: string;
32
+ status: string;
33
+ logs: string;
34
+ lineCount: number;
35
+ logType: "output" | "error";
36
+ logPath?: string;
37
+ }
38
+ export interface FlushToolResult {
39
+ flushed: boolean;
40
+ name?: string;
41
+ all?: boolean;
42
+ message: string;
43
+ }
44
+ export declare function handleLogsTool(input: LogsToolInput): Promise<LogsToolResult | FlushToolResult>;
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ import { executeLogs, executeFlush } from "../../cli/commands/logs.js";
3
+ export const logsToolName = "servherd_logs";
4
+ export const logsToolDescription = "Get recent log output from a server. " +
5
+ "Use this tool to debug issues, monitor server activity, or check for errors in server output. " +
6
+ "Can retrieve standard output logs (default) or error logs (stderr) by setting the error parameter. " +
7
+ "Returns the server name, status, log content as a string, line count, log type (output or error), and the log file path. " +
8
+ "Default is 50 lines but can be adjusted with the lines parameter. " +
9
+ "Use the since parameter to filter logs by time (e.g., '1h', '30m', '2024-01-15'). " +
10
+ "Use the head parameter to get first N lines instead of last N lines. " +
11
+ "Use flush=true to clear logs instead of retrieving them.";
12
+ export const logsToolSchema = z.object({
13
+ name: z.string().optional().describe("Name of the server to get logs for, e.g., 'frontend-dev' or 'api-server'. Required unless using flush with all=true."),
14
+ lines: z.number().optional().describe("Number of lines to retrieve from the end of the log. Defaults to 50. Ignored if head is specified."),
15
+ error: z.boolean().optional().describe("Set to true to get error logs (stderr) instead of standard output logs"),
16
+ since: z.string().optional().describe("Filter logs since this time. Accepts duration (e.g., '1h', '30m', '2d') or ISO date (e.g., '2024-01-15')"),
17
+ head: z.number().optional().describe("Get first N lines instead of last N lines"),
18
+ flush: z.boolean().optional().describe("Set to true to clear/flush logs instead of retrieving them"),
19
+ all: z.boolean().optional().describe("When flush=true, set this to true to flush logs for all servers"),
20
+ });
21
+ export async function handleLogsTool(input) {
22
+ // Handle flush mode
23
+ if (input.flush) {
24
+ const result = await executeFlush({
25
+ name: input.name,
26
+ all: input.all,
27
+ });
28
+ return {
29
+ flushed: result.flushed,
30
+ name: result.name,
31
+ all: result.all,
32
+ message: result.message,
33
+ };
34
+ }
35
+ // Handle normal logs mode
36
+ const result = await executeLogs({
37
+ name: input.name,
38
+ lines: input.lines,
39
+ error: input.error,
40
+ since: input.since,
41
+ head: input.head,
42
+ });
43
+ const logType = input.error ? "error" : "output";
44
+ const logPath = input.error ? result.errLogPath : result.outLogPath;
45
+ // Count actual lines in the logs
46
+ const lineCount = result.logs ? result.logs.split("\n").filter((line) => line.length > 0).length : 0;
47
+ return {
48
+ name: result.name,
49
+ status: result.status,
50
+ logs: result.logs,
51
+ lineCount,
52
+ logType,
53
+ logPath,
54
+ };
55
+ }
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ export declare const refreshToolName = "servherd_refresh";
3
+ export declare const refreshToolDescription: string;
4
+ export declare const refreshToolSchema: z.ZodObject<{
5
+ name: z.ZodOptional<z.ZodString>;
6
+ tag: z.ZodOptional<z.ZodString>;
7
+ all: z.ZodOptional<z.ZodBoolean>;
8
+ dryRun: z.ZodOptional<z.ZodBoolean>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ name?: string | undefined;
11
+ tag?: string | undefined;
12
+ all?: boolean | undefined;
13
+ dryRun?: boolean | undefined;
14
+ }, {
15
+ name?: string | undefined;
16
+ tag?: string | undefined;
17
+ all?: boolean | undefined;
18
+ dryRun?: boolean | undefined;
19
+ }>;
20
+ export type RefreshToolInput = z.infer<typeof refreshToolSchema>;
21
+ export interface RefreshResult {
22
+ name: string;
23
+ success: boolean;
24
+ status?: string;
25
+ message?: string;
26
+ driftDetails?: string;
27
+ skipped?: boolean;
28
+ }
29
+ export interface RefreshToolResult {
30
+ results: RefreshResult[];
31
+ summary: string;
32
+ }
33
+ export declare function handleRefreshTool(input: RefreshToolInput): Promise<RefreshToolResult>;
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { executeRefresh } from "../../cli/commands/refresh.js";
3
+ export const refreshToolName = "servherd_refresh";
4
+ export const refreshToolDescription = "Refresh servers with updated configuration. " +
5
+ "Use this tool when global config values (like hostname, httpsCert, httpsKey) have changed and you want to apply those changes to running servers. " +
6
+ "Servers that use config values in their command templates will be restarted with the new values. " +
7
+ "Use the dryRun option to preview which servers would be affected without making changes. " +
8
+ "Returns a list of results for each server with success status and a summary message.";
9
+ export const refreshToolSchema = z.object({
10
+ name: z.string().optional().describe("Name of a specific server to refresh, e.g., 'frontend-dev' or 'api-server'"),
11
+ tag: z.string().optional().describe("Refresh all servers with this tag that have drift, e.g., 'frontend' or 'development'"),
12
+ all: z.boolean().optional().describe("Set to true to refresh all servers with drift"),
13
+ dryRun: z.boolean().optional().describe("Set to true to preview what would be refreshed without making changes"),
14
+ });
15
+ export async function handleRefreshTool(input) {
16
+ const commandResults = await executeRefresh({
17
+ name: input.name,
18
+ tag: input.tag,
19
+ all: input.all,
20
+ dryRun: input.dryRun,
21
+ });
22
+ const results = commandResults.map((r) => ({
23
+ name: r.name,
24
+ success: r.success,
25
+ status: r.status,
26
+ message: r.message,
27
+ driftDetails: r.driftDetails,
28
+ skipped: r.skipped,
29
+ }));
30
+ // Check if this is a "no drift" result
31
+ if (results.length === 1 && results[0].skipped && results[0].name === "") {
32
+ return {
33
+ results: [],
34
+ summary: results[0].message || "No servers have config drift",
35
+ };
36
+ }
37
+ const successCount = results.filter((r) => r.success && !r.skipped).length;
38
+ const skippedCount = results.filter((r) => r.skipped).length;
39
+ const failedCount = results.filter((r) => !r.success).length;
40
+ let summary;
41
+ if (input.dryRun) {
42
+ summary = `Dry run: ${skippedCount} server${skippedCount !== 1 ? "s" : ""} would be refreshed`;
43
+ }
44
+ else if (failedCount > 0) {
45
+ summary = `Refreshed ${successCount} server${successCount !== 1 ? "s" : ""}, ${failedCount} failed`;
46
+ }
47
+ else {
48
+ summary = `Successfully refreshed ${successCount} server${successCount !== 1 ? "s" : ""}`;
49
+ }
50
+ return {
51
+ results,
52
+ summary,
53
+ };
54
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import type { RemoveResult } from "../../cli/output/formatters.js";
3
+ export declare const removeToolName = "servherd_remove";
4
+ export declare const removeToolDescription: string;
5
+ export declare const removeToolSchema: z.ZodObject<{
6
+ name: z.ZodOptional<z.ZodString>;
7
+ all: z.ZodOptional<z.ZodBoolean>;
8
+ tag: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ name?: string | undefined;
11
+ tag?: string | undefined;
12
+ all?: boolean | undefined;
13
+ }, {
14
+ name?: string | undefined;
15
+ tag?: string | undefined;
16
+ all?: boolean | undefined;
17
+ }>;
18
+ export type RemoveToolInput = z.infer<typeof removeToolSchema>;
19
+ export interface RemoveToolResult {
20
+ results: RemoveResult[];
21
+ summary: string;
22
+ }
23
+ export declare function handleRemoveTool(input: RemoveToolInput): Promise<RemoveToolResult>;
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { executeRemove } from "../../cli/commands/remove.js";
3
+ export const removeToolName = "servherd_remove";
4
+ export const removeToolDescription = "Permanently remove servers from the registry and process management. " +
5
+ "Use this tool when a server is no longer needed and should be completely removed, not just stopped. " +
6
+ "This stops the server process, removes it from PM2, and deletes it from the servherd registry. " +
7
+ "Can target a specific server by name, all servers with a particular tag, or all managed servers at once. " +
8
+ "Returns a list of results for each server with success status and a summary message. " +
9
+ "Unlike servherd_stop, removed servers cannot be restarted without starting them again from scratch.";
10
+ export const removeToolSchema = z.object({
11
+ name: z.string().optional().describe("Name of the server to remove, e.g., 'frontend-dev' or 'brave-tiger'"),
12
+ all: z.boolean().optional().describe("Set to true to remove all managed servers"),
13
+ tag: z.string().optional().describe("Remove all servers with this tag, e.g., 'temporary' or 'test'"),
14
+ });
15
+ export async function handleRemoveTool(input) {
16
+ // Validate input
17
+ if (!input.name && !input.all && !input.tag) {
18
+ throw new Error("Either name, all, or tag must be provided");
19
+ }
20
+ // Always force in MCP context since we can't prompt
21
+ const results = await executeRemove({
22
+ name: input.name,
23
+ all: input.all,
24
+ tag: input.tag,
25
+ force: true, // MCP context can't prompt for confirmation
26
+ });
27
+ const successCount = results.filter((r) => r.success).length;
28
+ const failCount = results.filter((r) => !r.success && !r.cancelled).length;
29
+ let summary;
30
+ if (results.length === 0) {
31
+ summary = "No servers found to remove";
32
+ }
33
+ else if (failCount === 0) {
34
+ summary = `Successfully removed ${successCount} server${successCount !== 1 ? "s" : ""}`;
35
+ }
36
+ else {
37
+ summary = `Removed ${successCount} server${successCount !== 1 ? "s" : ""}, ${failCount} failed`;
38
+ }
39
+ return {
40
+ results,
41
+ summary,
42
+ };
43
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import type { RestartResult } from "../../cli/commands/restart.js";
3
+ export declare const restartToolName = "servherd_restart";
4
+ export declare const restartToolDescription: string;
5
+ export declare const restartToolSchema: z.ZodObject<{
6
+ name: z.ZodOptional<z.ZodString>;
7
+ all: z.ZodOptional<z.ZodBoolean>;
8
+ tag: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ name?: string | undefined;
11
+ tag?: string | undefined;
12
+ all?: boolean | undefined;
13
+ }, {
14
+ name?: string | undefined;
15
+ tag?: string | undefined;
16
+ all?: boolean | undefined;
17
+ }>;
18
+ export type RestartToolInput = z.infer<typeof restartToolSchema>;
19
+ export interface RestartToolResult {
20
+ results: RestartResult[];
21
+ summary: string;
22
+ }
23
+ export declare function handleRestartTool(input: RestartToolInput): Promise<RestartToolResult>;
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ import { executeRestart } from "../../cli/commands/restart.js";
3
+ export const restartToolName = "servherd_restart";
4
+ export const restartToolDescription = "Restart one or more development servers. " +
5
+ "Use this tool when servers need to pick up configuration changes, clear memory, or recover from an error state. " +
6
+ "Can target a specific server by name, all servers with a particular tag, or all managed servers at once. " +
7
+ "Returns a list of results for each server with success status and a summary message. " +
8
+ "Servers maintain their assigned ports and configuration across restarts.";
9
+ export const restartToolSchema = z.object({
10
+ name: z.string().optional().describe("Name of the server to restart, e.g., 'frontend-dev' or 'api-server'"),
11
+ all: z.boolean().optional().describe("Set to true to restart all managed servers"),
12
+ tag: z.string().optional().describe("Restart all servers with this tag, e.g., 'backend' or 'production'"),
13
+ });
14
+ export async function handleRestartTool(input) {
15
+ // Validate input
16
+ if (!input.name && !input.all && !input.tag) {
17
+ throw new Error("Either name, all, or tag must be provided");
18
+ }
19
+ const result = await executeRestart({
20
+ name: input.name,
21
+ all: input.all,
22
+ tag: input.tag,
23
+ });
24
+ // Normalize to array
25
+ const results = Array.isArray(result) ? result : [result];
26
+ const successCount = results.filter((r) => r.success).length;
27
+ const failCount = results.length - successCount;
28
+ let summary;
29
+ if (results.length === 0) {
30
+ summary = "No servers found to restart";
31
+ }
32
+ else if (failCount === 0) {
33
+ summary = `Successfully restarted ${successCount} server${successCount !== 1 ? "s" : ""}`;
34
+ }
35
+ else {
36
+ summary = `Restarted ${successCount} server${successCount !== 1 ? "s" : ""}, ${failCount} failed`;
37
+ }
38
+ return {
39
+ results,
40
+ summary,
41
+ };
42
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ export declare const startToolName = "servherd_start";
3
+ export declare const startToolDescription: string;
4
+ export declare const startToolSchema: z.ZodObject<{
5
+ command: z.ZodString;
6
+ cwd: z.ZodOptional<z.ZodString>;
7
+ name: z.ZodOptional<z.ZodString>;
8
+ tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
+ description: z.ZodOptional<z.ZodString>;
10
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ command: string;
13
+ name?: string | undefined;
14
+ cwd?: string | undefined;
15
+ env?: Record<string, string> | undefined;
16
+ tags?: string[] | undefined;
17
+ description?: string | undefined;
18
+ }, {
19
+ command: string;
20
+ name?: string | undefined;
21
+ cwd?: string | undefined;
22
+ env?: Record<string, string> | undefined;
23
+ tags?: string[] | undefined;
24
+ description?: string | undefined;
25
+ }>;
26
+ export type StartToolInput = z.infer<typeof startToolSchema>;
27
+ export interface StartToolResult {
28
+ action: "started" | "existing" | "restarted" | "renamed";
29
+ name: string;
30
+ port: number;
31
+ url: string;
32
+ status: string;
33
+ message: string;
34
+ portReassigned?: boolean;
35
+ originalPort?: number;
36
+ previousName?: string;
37
+ }
38
+ export declare function handleStartTool(input: StartToolInput): Promise<StartToolResult>;
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import { executeStart } from "../../cli/commands/start.js";
3
+ import { ConfigService } from "../../services/config.service.js";
4
+ import { findMissingVariables, getTemplateVariables, formatMissingVariablesForMCP, } from "../../utils/template.js";
5
+ import { ServherdError, ServherdErrorCode } from "../../types/errors.js";
6
+ export const startToolName = "servherd_start";
7
+ export const startToolDescription = "Start a development server with automatic port assignment and process management. " +
8
+ "Use this tool when you need to launch a new server process or verify an existing server is running. " +
9
+ "The command can include {{port}}, {{hostname}}, and {{url}} template variables that will be substituted with actual values. " +
10
+ "Returns the server name, assigned port, full URL, and status (started, existing, or restarted). " +
11
+ "If an identical server is already running, it will return the existing server details rather than starting a duplicate.";
12
+ export const startToolSchema = z.object({
13
+ command: z.string().describe("The command to run, e.g., 'npm start --port {{port}}' or 'python -m http.server {{port}}'"),
14
+ cwd: z.string().optional().describe("Working directory for the server, e.g., '/home/user/my-project'. Defaults to current directory"),
15
+ name: z.string().optional().describe("Human-readable name for the server, e.g., 'frontend-dev' or 'api-server'. Auto-generated if not provided"),
16
+ tags: z.array(z.string()).optional().describe("Tags for filtering/grouping servers, e.g., ['frontend', 'development']"),
17
+ description: z.string().optional().describe("Description of the server's purpose, e.g., 'React development server for the dashboard'"),
18
+ env: z.record(z.string()).optional().describe("Environment variables, e.g., {\"NODE_ENV\": \"development\", \"API_URL\": \"http://localhost:{{port}}\"}"),
19
+ });
20
+ export async function handleStartTool(input) {
21
+ // Load config and check for missing template variables before starting
22
+ const configService = new ConfigService();
23
+ const config = await configService.load();
24
+ // Use a placeholder port (0) to check for missing config-based variables
25
+ // The actual port will be assigned by PortService during executeStart
26
+ const templateVars = getTemplateVariables(config, 0);
27
+ const missingVars = findMissingVariables(input.command, templateVars);
28
+ // Filter to only configurable missing variables (ignore port/url which are auto-generated)
29
+ const configurableMissing = missingVars.filter(v => v.configurable);
30
+ if (configurableMissing.length > 0) {
31
+ const errorMessage = formatMissingVariablesForMCP(configurableMissing);
32
+ throw new ServherdError(ServherdErrorCode.CONFIG_VALIDATION_FAILED, errorMessage);
33
+ }
34
+ const result = await executeStart({
35
+ command: input.command,
36
+ cwd: input.cwd,
37
+ name: input.name,
38
+ tags: input.tags,
39
+ description: input.description,
40
+ env: input.env,
41
+ });
42
+ const url = `${result.server.protocol}://${result.server.hostname}:${result.server.port}`;
43
+ let message;
44
+ switch (result.action) {
45
+ case "started":
46
+ message = `Server "${result.server.name}" started at ${url}`;
47
+ break;
48
+ case "existing":
49
+ message = `Server "${result.server.name}" is already running at ${url}`;
50
+ break;
51
+ case "restarted":
52
+ message = `Server "${result.server.name}" restarted at ${url}`;
53
+ break;
54
+ case "renamed":
55
+ message = `Server renamed from "${result.previousName}" to "${result.server.name}" at ${url}`;
56
+ break;
57
+ }
58
+ // Add port reassignment info to message if applicable
59
+ if (result.portReassigned) {
60
+ message += ` (port ${result.originalPort} was unavailable, reassigned to ${result.server.port})`;
61
+ }
62
+ return {
63
+ action: result.action,
64
+ name: result.server.name,
65
+ port: result.server.port,
66
+ url,
67
+ status: result.status,
68
+ message,
69
+ portReassigned: result.portReassigned,
70
+ originalPort: result.originalPort,
71
+ previousName: result.previousName,
72
+ };
73
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import type { StopResult } from "../../cli/output/formatters.js";
3
+ export declare const stopToolName = "servherd_stop";
4
+ export declare const stopToolDescription: string;
5
+ export declare const stopToolSchema: z.ZodObject<{
6
+ name: z.ZodOptional<z.ZodString>;
7
+ all: z.ZodOptional<z.ZodBoolean>;
8
+ tag: z.ZodOptional<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ name?: string | undefined;
11
+ tag?: string | undefined;
12
+ all?: boolean | undefined;
13
+ }, {
14
+ name?: string | undefined;
15
+ tag?: string | undefined;
16
+ all?: boolean | undefined;
17
+ }>;
18
+ export type StopToolInput = z.infer<typeof stopToolSchema>;
19
+ export interface StopToolResult {
20
+ results: StopResult[];
21
+ summary: string;
22
+ }
23
+ export declare function handleStopTool(input: StopToolInput): Promise<StopToolResult>;
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { executeStop } from "../../cli/commands/stop.js";
3
+ export const stopToolName = "servherd_stop";
4
+ export const stopToolDescription = "Stop one or more running development servers. " +
5
+ "Use this tool when you need to gracefully shut down servers that are no longer needed or before making configuration changes. " +
6
+ "Can target a specific server by name, all servers with a particular tag, or all managed servers at once. " +
7
+ "Returns a list of results for each server with success status and a summary message. " +
8
+ "Stopped servers remain in the registry and can be restarted later with servherd_start.";
9
+ export const stopToolSchema = z.object({
10
+ name: z.string().optional().describe("Name of the server to stop, e.g., 'frontend-dev' or 'brave-tiger'"),
11
+ all: z.boolean().optional().describe("Set to true to stop all managed servers"),
12
+ tag: z.string().optional().describe("Stop all servers with this tag, e.g., 'frontend' or 'development'"),
13
+ });
14
+ export async function handleStopTool(input) {
15
+ // Validate input
16
+ if (!input.name && !input.all && !input.tag) {
17
+ throw new Error("Either name, all, or tag must be provided");
18
+ }
19
+ const results = await executeStop({
20
+ name: input.name,
21
+ all: input.all,
22
+ tag: input.tag,
23
+ });
24
+ const successCount = results.filter((r) => r.success).length;
25
+ const failCount = results.length - successCount;
26
+ let summary;
27
+ if (results.length === 0) {
28
+ summary = "No servers found to stop";
29
+ }
30
+ else if (failCount === 0) {
31
+ summary = `Successfully stopped ${successCount} server${successCount !== 1 ? "s" : ""}`;
32
+ }
33
+ else {
34
+ summary = `Stopped ${successCount} server${successCount !== 1 ? "s" : ""}, ${failCount} failed`;
35
+ }
36
+ return {
37
+ results,
38
+ summary,
39
+ };
40
+ }