• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / servherd / 20961701317

13 Jan 2026 03:07PM UTC coverage: 82.727% (+1.2%) from 81.563%
20961701317

push

github

apowers313
test: improved test coverage to 81.56%

901 of 1027 branches covered (87.73%)

Branch coverage included in aggregate %.

3601 of 4415 relevant lines covered (81.56%)

13.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.26
/src/mcp/tools/start.ts
1
import { z } from "zod";
1✔
2
import { executeStart } from "../../cli/commands/start.js";
3
import type { StartCommandResult } from "../../cli/commands/start.js";
4
import { ConfigService } from "../../services/config.service.js";
5
import {
6
  findMissingVariables,
7
  getTemplateVariables,
8
  formatMissingVariablesForMCP,
9
} from "../../utils/template.js";
10
import { ServherdError, ServherdErrorCode } from "../../types/errors.js";
11

12
export const startToolName = "servherd_start";
1✔
13

14
export const startToolDescription =
1✔
15
  "Start a development server with automatic port assignment and process management. " +
1✔
16
  "Server identity is determined by working directory + name. " +
17
  "Without a name, a deterministic name is generated from command + env variables, so different command/env = different server. " +
18
  "With an explicit name, you can update the command/env for an existing server (it will restart with the new config). " +
19
  "The command can include {{port}}, {{hostname}}, and {{url}} template variables that will be substituted with actual values. " +
20
  "Returns the server name, assigned port, full URL, and status (started, existing, or restarted).";
21

22
export const startToolSchema = z.object({
1✔
23
  command: z.string().describe("The command to run, e.g., 'npm start --port {{port}}' or 'python -m http.server {{port}}'"),
1✔
24
  cwd: z.string().optional().describe("Working directory for the server, e.g., '/home/user/my-project'. Defaults to current directory"),
1✔
25
  name: z.string().optional().describe("Human-readable name for the server, e.g., 'frontend-dev' or 'api-server'. With explicit name, you can update command/env and the same server will be reused. Auto-generated deterministically from command+env if not provided"),
1✔
26
  port: z.number().optional().describe("Explicit port number to use instead of auto-assigned"),
1✔
27
  protocol: z.enum(["http", "https"]).optional().describe("Protocol to use (http or https). Defaults to global config setting"),
1✔
28
  tags: z.array(z.string()).optional().describe("Tags for filtering/grouping servers, e.g., ['frontend', 'development']"),
1✔
29
  description: z.string().optional().describe("Description of the server's purpose, e.g., 'React development server for the dashboard'"),
1✔
30
  env: z.record(z.string()).optional().describe("Environment variables, e.g., {\"NODE_ENV\": \"development\", \"API_URL\": \"http://localhost:{{port}}\"}"),
1✔
31
});
1✔
32

33
export type StartToolInput = z.infer<typeof startToolSchema>;
34

35
export interface StartToolResult {
36
  action: "started" | "existing" | "restarted" | "renamed" | "refreshed";
37
  name: string;
38
  port: number;
39
  url: string;
40
  status: string;
41
  message: string;
42
  portReassigned?: boolean;
43
  originalPort?: number;
44
  previousName?: string;
45
  /** Whether config drift was detected and applied */
46
  configDrift?: boolean;
47
  /** Details of config drift that was applied */
48
  driftDetails?: string[];
49
}
50

51
export async function handleStartTool(input: StartToolInput): Promise<StartToolResult> {
4✔
52
  // Load config and check for missing template variables before starting
53
  const configService = new ConfigService();
4✔
54
  const config = await configService.load();
4✔
55

56
  // Use a placeholder port (0) to check for missing config-based variables
57
  // The actual port will be assigned by PortService during executeStart
58
  const templateVars = getTemplateVariables(config, 0);
4✔
59
  const missingVars = findMissingVariables(input.command, templateVars);
4✔
60

61
  // Filter to only configurable missing variables (ignore port/url which are auto-generated)
62
  const configurableMissing = missingVars.filter(v => v.configurable);
4✔
63

64
  if (configurableMissing.length > 0) {
4!
65
    const errorMessage = formatMissingVariablesForMCP(configurableMissing);
×
66
    throw new ServherdError(
×
67
      ServherdErrorCode.CONFIG_VALIDATION_FAILED,
×
68
      errorMessage,
×
69
    );
×
70
  }
×
71

72
  const result: StartCommandResult = await executeStart({
4✔
73
    command: input.command,
4✔
74
    cwd: input.cwd,
4✔
75
    name: input.name,
4✔
76
    port: input.port,
4✔
77
    protocol: input.protocol,
4✔
78
    tags: input.tags,
4✔
79
    description: input.description,
4✔
80
    env: input.env,
4✔
81
  });
4✔
82

83
  const url = `${result.server.protocol}://${result.server.hostname}:${result.server.port}`;
4✔
84

85
  let message: string;
4✔
86
  switch (result.action) {
4✔
87
    case "started":
4✔
88
      message = `Server "${result.server.name}" started at ${url}`;
3✔
89
      break;
3✔
90
    case "existing":
4✔
91
      message = `Server "${result.server.name}" is already running at ${url}`;
1✔
92
      break;
1✔
93
    case "restarted":
4!
94
      message = `Server "${result.server.name}" restarted at ${url}`;
×
95
      break;
×
96
    case "renamed":
4!
97
      message = `Server renamed from "${result.previousName}" to "${result.server.name}" at ${url}`;
×
98
      break;
×
99
    case "refreshed":
4!
100
      message = `Server "${result.server.name}" refreshed with new config at ${url}`;
×
101
      break;
×
102
  }
4✔
103

104
  // Add port reassignment info to message if applicable
105
  if (result.portReassigned) {
4!
106
    message += ` (port ${result.originalPort} was unavailable, reassigned to ${result.server.port})`;
×
107
  }
×
108

109
  // Add drift details to message if applicable
110
  if (result.driftDetails && result.driftDetails.length > 0) {
4!
111
    message += `. Config changes: ${result.driftDetails.join(", ")}`;
×
112
  }
×
113

114
  return {
4✔
115
    action: result.action,
4✔
116
    name: result.server.name,
4✔
117
    port: result.server.port,
4✔
118
    url,
4✔
119
    status: result.status,
4✔
120
    message,
4✔
121
    portReassigned: result.portReassigned,
4✔
122
    originalPort: result.originalPort,
4✔
123
    previousName: result.previousName,
4✔
124
    configDrift: result.configDrift,
4✔
125
    driftDetails: result.driftDetails,
4✔
126
  };
4✔
127
}
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc