@agimon-ai/mcp-proxy 0.8.0 → 0.9.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.
@@ -20,14 +20,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
20
20
  enumerable: true
21
21
  }) : target, mod));
22
22
  //#endregion
23
- let _agimon_ai_foundation_validator = require("@agimon-ai/foundation-validator");
24
- let _modelcontextprotocol_sdk_server_index_js = require("@modelcontextprotocol/sdk/server/index.js");
25
- let _modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
26
- let zod = require("zod");
27
23
  let node_fs = require("node:fs");
28
24
  let node_fs_promises = require("node:fs/promises");
29
25
  let js_yaml = require("js-yaml");
30
26
  js_yaml = __toESM(js_yaml);
27
+ let zod = require("zod");
31
28
  let node_crypto = require("node:crypto");
32
29
  let node_os = require("node:os");
33
30
  let node_path = require("node:path");
@@ -38,16 +35,19 @@ let _modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/s
38
35
  let _modelcontextprotocol_sdk_client_streamableHttp_js = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
39
36
  let node_child_process = require("node:child_process");
40
37
  let liquidjs = require("liquidjs");
38
+ let _agimon_ai_foundation_validator = require("@agimon-ai/foundation-validator");
41
39
  let node_events = require("node:events");
42
40
  let node_http = require("node:http");
43
41
  let node_util = require("node:util");
44
42
  let _hono_node_server = require("@hono/node-server");
45
43
  let _modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
44
+ let _modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
46
45
  let hono = require("hono");
47
46
  let _modelcontextprotocol_sdk_server_sse_js = require("@modelcontextprotocol/sdk/server/sse.js");
48
47
  let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
48
+ let _modelcontextprotocol_sdk_server_index_js = require("@modelcontextprotocol/sdk/server/index.js");
49
49
  //#region package.json
50
- var version = "0.7.3";
50
+ var version = "0.9.0";
51
51
  //#endregion
52
52
  //#region src/utils/mcpConfigSchema.ts
53
53
  /**
@@ -966,25 +966,68 @@ function findConfigFile() {
966
966
  return null;
967
967
  }
968
968
  //#endregion
969
- //#region src/utils/parseToolName.ts
969
+ //#region src/utils/generateServerId.ts
970
970
  /**
971
- * Parse tool name to extract server and actual tool name
972
- * Supports both plain tool names and prefixed format: {serverName}__{toolName}
971
+ * generateServerId Utilities
973
972
  *
974
- * @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
975
- * @returns Parsed result with optional serverName and actualToolName
973
+ * DESIGN PATTERNS:
974
+ * - Pure functions with no side effects
975
+ * - Single responsibility per function
976
+ * - Functional programming approach
977
+ *
978
+ * CODING STANDARDS:
979
+ * - Export individual functions, not classes
980
+ * - Use descriptive function names with verbs
981
+ * - Add JSDoc comments for complex logic
982
+ * - Keep functions small and focused
983
+ *
984
+ * AVOID:
985
+ * - Side effects (mutating external state)
986
+ * - Stateful logic (use services for state)
987
+ * - Complex external dependencies
988
+ */
989
+ /**
990
+ * Character set for generating human-readable IDs.
991
+ * Excludes confusing characters: 0, O, 1, l, I
992
+ */
993
+ const CHARSET = "23456789abcdefghjkmnpqrstuvwxyz";
994
+ /**
995
+ * Default length for generated server IDs (6 characters)
996
+ */
997
+ const DEFAULT_ID_LENGTH = 6;
998
+ /**
999
+ * Generate a short, human-readable server ID.
1000
+ *
1001
+ * Uses Node.js crypto.randomBytes for cryptographically secure randomness
1002
+ * with rejection sampling to avoid modulo bias.
1003
+ *
1004
+ * The generated ID:
1005
+ * - Is 6 characters long by default
1006
+ * - Uses only lowercase alphanumeric characters
1007
+ * - Excludes confusing characters (0, O, 1, l, I)
1008
+ *
1009
+ * @param length - Length of the ID to generate (default: 6)
1010
+ * @returns A random, human-readable ID
976
1011
  *
977
1012
  * @example
978
- * parseToolName("my_tool") // { actualToolName: "my_tool" }
979
- * parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
1013
+ * generateServerId() // "abc234"
1014
+ * generateServerId(4) // "x7mn"
980
1015
  */
981
- function parseToolName(toolName) {
982
- const separatorIndex = toolName.indexOf("__");
983
- if (separatorIndex > 0) return {
984
- serverName: toolName.substring(0, separatorIndex),
985
- actualToolName: toolName.substring(separatorIndex + 2)
986
- };
987
- return { actualToolName: toolName };
1016
+ function generateServerId(length = DEFAULT_ID_LENGTH) {
1017
+ const charsetLength = 31;
1018
+ const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength - 1;
1019
+ let result = "";
1020
+ let remaining = length;
1021
+ while (remaining > 0) {
1022
+ const bytes = (0, node_crypto.randomBytes)(remaining);
1023
+ for (let i = 0; i < bytes.length && remaining > 0; i++) {
1024
+ const byte = bytes[i];
1025
+ if (byte > maxUnbiased) continue;
1026
+ result += CHARSET[byte % charsetLength];
1027
+ remaining--;
1028
+ }
1029
+ }
1030
+ return result;
988
1031
  }
989
1032
  //#endregion
990
1033
  //#region src/utils/parseFrontMatter.ts
@@ -1120,68 +1163,25 @@ function extractSkillFrontMatter(content) {
1120
1163
  return null;
1121
1164
  }
1122
1165
  //#endregion
1123
- //#region src/utils/generateServerId.ts
1124
- /**
1125
- * generateServerId Utilities
1126
- *
1127
- * DESIGN PATTERNS:
1128
- * - Pure functions with no side effects
1129
- * - Single responsibility per function
1130
- * - Functional programming approach
1131
- *
1132
- * CODING STANDARDS:
1133
- * - Export individual functions, not classes
1134
- * - Use descriptive function names with verbs
1135
- * - Add JSDoc comments for complex logic
1136
- * - Keep functions small and focused
1137
- *
1138
- * AVOID:
1139
- * - Side effects (mutating external state)
1140
- * - Stateful logic (use services for state)
1141
- * - Complex external dependencies
1142
- */
1143
- /**
1144
- * Character set for generating human-readable IDs.
1145
- * Excludes confusing characters: 0, O, 1, l, I
1146
- */
1147
- const CHARSET = "23456789abcdefghjkmnpqrstuvwxyz";
1148
- /**
1149
- * Default length for generated server IDs (6 characters)
1150
- */
1151
- const DEFAULT_ID_LENGTH = 6;
1166
+ //#region src/utils/parseToolName.ts
1152
1167
  /**
1153
- * Generate a short, human-readable server ID.
1154
- *
1155
- * Uses Node.js crypto.randomBytes for cryptographically secure randomness
1156
- * with rejection sampling to avoid modulo bias.
1157
- *
1158
- * The generated ID:
1159
- * - Is 6 characters long by default
1160
- * - Uses only lowercase alphanumeric characters
1161
- * - Excludes confusing characters (0, O, 1, l, I)
1168
+ * Parse tool name to extract server and actual tool name
1169
+ * Supports both plain tool names and prefixed format: {serverName}__{toolName}
1162
1170
  *
1163
- * @param length - Length of the ID to generate (default: 6)
1164
- * @returns A random, human-readable ID
1171
+ * @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
1172
+ * @returns Parsed result with optional serverName and actualToolName
1165
1173
  *
1166
1174
  * @example
1167
- * generateServerId() // "abc234"
1168
- * generateServerId(4) // "x7mn"
1175
+ * parseToolName("my_tool") // { actualToolName: "my_tool" }
1176
+ * parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
1169
1177
  */
1170
- function generateServerId(length = DEFAULT_ID_LENGTH) {
1171
- const charsetLength = 31;
1172
- const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength - 1;
1173
- let result = "";
1174
- let remaining = length;
1175
- while (remaining > 0) {
1176
- const bytes = (0, node_crypto.randomBytes)(remaining);
1177
- for (let i = 0; i < bytes.length && remaining > 0; i++) {
1178
- const byte = bytes[i];
1179
- if (byte > maxUnbiased) continue;
1180
- result += CHARSET[byte % charsetLength];
1181
- remaining--;
1182
- }
1183
- }
1184
- return result;
1178
+ function parseToolName(toolName) {
1179
+ const separatorIndex = toolName.indexOf("__");
1180
+ if (separatorIndex > 0) return {
1181
+ serverName: toolName.substring(0, separatorIndex),
1182
+ actualToolName: toolName.substring(separatorIndex + 2)
1183
+ };
1184
+ return { actualToolName: toolName };
1185
1185
  }
1186
1186
  //#endregion
1187
1187
  //#region src/services/DefinitionsCacheService.ts
@@ -1894,304 +1894,440 @@ var McpClientManagerService = class {
1894
1894
  return this.clients.has(serverName);
1895
1895
  }
1896
1896
  };
1897
+ /** pnpx command name (pnpm's npx equivalent) */
1898
+ const COMMAND_PNPX = "pnpx";
1899
+ /** pnpm command name */
1900
+ const COMMAND_PNPM = "pnpm";
1901
+ /** Tool subcommand for uv */
1902
+ const ARG_TOOL = "tool";
1903
+ /** Install subcommand for uv tool and npm/pnpm */
1904
+ const ARG_INSTALL = "install";
1905
+ /**
1906
+ * Regex pattern for valid package names (npm, pnpm, uvx, uv)
1907
+ * Allows: @scope/package-name@version, package-name, package_name
1908
+ * Prevents shell metacharacters that could enable command injection
1909
+ * @example
1910
+ * // Valid: '@scope/package@1.0.0', 'my-package', 'my_package', '@org/pkg'
1911
+ * // Invalid: 'pkg; rm -rf /', 'pkg$(cmd)', 'pkg`whoami`', 'pkg|cat /etc/passwd'
1912
+ */
1913
+ const VALID_PACKAGE_NAME_PATTERN = /^(@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
1914
+ /** Windows platform identifier */
1915
+ const PLATFORM_WIN32 = "win32";
1916
+ /** Stdio option to ignore stream */
1917
+ const STDIO_IGNORE = "ignore";
1918
+ /** Stdio option to pipe stream */
1919
+ const STDIO_PIPE = "pipe";
1897
1920
  //#endregion
1898
- //#region src/services/RuntimeStateService.ts
1921
+ //#region src/services/PrefetchService/PrefetchService.ts
1899
1922
  /**
1900
- * RuntimeStateService
1923
+ * PrefetchService
1901
1924
  *
1902
- * Persists runtime metadata for HTTP mcp-proxy instances so external commands
1903
- * (for example `mcp-proxy stop`) can discover and target the correct server.
1925
+ * DESIGN PATTERNS:
1926
+ * - Service pattern for business logic encapsulation
1927
+ * - Single responsibility principle
1928
+ *
1929
+ * CODING STANDARDS:
1930
+ * - Use async/await for asynchronous operations
1931
+ * - Throw descriptive errors for error cases
1932
+ * - Keep methods focused and well-named
1933
+ * - Document complex logic with comments
1934
+ *
1935
+ * AVOID:
1936
+ * - Mixing concerns (keep focused on single domain)
1937
+ * - Direct tool implementation (services should be tool-agnostic)
1904
1938
  */
1905
- const RUNTIME_DIR_NAME = "runtimes";
1906
- const RUNTIME_FILE_SUFFIX = ".runtime.json";
1907
- function isObject(value) {
1908
- return typeof value === "object" && value !== null;
1909
- }
1910
- function isRuntimeStateRecord(value) {
1911
- if (!isObject(value)) return false;
1912
- return typeof value.serverId === "string" && typeof value.host === "string" && typeof value.port === "number" && value.transport === "http" && typeof value.shutdownToken === "string" && typeof value.startedAt === "string" && typeof value.pid === "number" && (value.configPath === void 0 || typeof value.configPath === "string");
1913
- }
1914
- function toErrorMessage$2(error) {
1915
- return error instanceof Error ? error.message : String(error);
1939
+ /**
1940
+ * Type guard to check if a config object is an McpStdioConfig
1941
+ * @param config - Config object to check
1942
+ * @returns True if config has required McpStdioConfig properties
1943
+ */
1944
+ function isMcpStdioConfig(config) {
1945
+ return typeof config === "object" && config !== null && "command" in config;
1916
1946
  }
1917
1947
  /**
1918
- * Runtime state persistence implementation.
1948
+ * PrefetchService handles pre-downloading packages used by MCP servers.
1949
+ * Supports npx (Node.js), uvx (Python/uv), and uv run commands.
1950
+ *
1951
+ * @example
1952
+ * ```typescript
1953
+ * const service = new PrefetchService({
1954
+ * mcpConfig: await configFetcher.fetchConfiguration(),
1955
+ * parallel: true,
1956
+ * });
1957
+ * const packages = service.extractPackages();
1958
+ * const summary = await service.prefetch();
1959
+ * ```
1919
1960
  */
1920
- var RuntimeStateService = class RuntimeStateService {
1921
- runtimeDir;
1922
- logger;
1923
- constructor(runtimeDir, logger = console) {
1924
- this.runtimeDir = runtimeDir ?? RuntimeStateService.getDefaultRuntimeDir();
1925
- this.logger = logger;
1926
- }
1927
- /**
1928
- * Resolve default runtime directory under the user's home cache path.
1929
- * @returns Absolute runtime directory path
1930
- */
1931
- static getDefaultRuntimeDir() {
1932
- return (0, node_path.join)((0, node_os.homedir)(), ".aicode-toolkit", "mcp-proxy", RUNTIME_DIR_NAME);
1933
- }
1961
+ var PrefetchService = class {
1962
+ config;
1934
1963
  /**
1935
- * Build runtime state file path for a given server ID.
1936
- * @param serverId - Target mcp-proxy server identifier
1937
- * @returns Absolute runtime file path
1964
+ * Creates a new PrefetchService instance
1965
+ * @param config - Service configuration options
1938
1966
  */
1939
- getRecordPath(serverId) {
1940
- return (0, node_path.join)(this.runtimeDir, `${serverId}${RUNTIME_FILE_SUFFIX}`);
1967
+ constructor(config) {
1968
+ this.config = config;
1941
1969
  }
1942
1970
  /**
1943
- * Persist a runtime state record.
1944
- * @param record - Runtime metadata to persist
1945
- * @returns Promise that resolves when write completes
1971
+ * Extract all prefetchable packages from the MCP configuration
1972
+ * @returns Array of package info objects
1946
1973
  */
1947
- async write(record) {
1948
- await (0, node_fs_promises.mkdir)(this.runtimeDir, { recursive: true });
1949
- await (0, node_fs_promises.writeFile)(this.getRecordPath(record.serverId), JSON.stringify(record, null, 2), "utf-8");
1974
+ extractPackages() {
1975
+ const packages = [];
1976
+ const { mcpConfig, filter } = this.config;
1977
+ for (const [serverName, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
1978
+ if (serverConfig.disabled) continue;
1979
+ if (serverConfig.transport !== "stdio") continue;
1980
+ if (!isMcpStdioConfig(serverConfig.config)) continue;
1981
+ const packageInfo = this.extractPackageInfo(serverName, serverConfig.config);
1982
+ if (packageInfo) {
1983
+ if (filter && packageInfo.packageManager !== filter) continue;
1984
+ packages.push(packageInfo);
1985
+ }
1986
+ }
1987
+ return packages;
1950
1988
  }
1951
1989
  /**
1952
- * Read a runtime state record by server ID.
1953
- * @param serverId - Target mcp-proxy server identifier
1954
- * @returns Matching runtime record, or null when no record exists
1990
+ * Prefetch all packages from the configuration
1991
+ * @returns Summary of prefetch results
1992
+ * @throws Error if prefetch operation fails unexpectedly
1955
1993
  */
1956
- async read(serverId) {
1957
- const filePath = this.getRecordPath(serverId);
1994
+ async prefetch() {
1958
1995
  try {
1959
- const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
1960
- const parsed = JSON.parse(content);
1961
- return isRuntimeStateRecord(parsed) ? parsed : null;
1996
+ const packages = this.extractPackages();
1997
+ const results = [];
1998
+ if (packages.length === 0) return {
1999
+ totalPackages: 0,
2000
+ successful: 0,
2001
+ failed: 0,
2002
+ results: []
2003
+ };
2004
+ if (this.config.parallel) {
2005
+ const promises = packages.map(async (pkg) => this.prefetchPackage(pkg));
2006
+ results.push(...await Promise.all(promises));
2007
+ } else for (const pkg of packages) {
2008
+ const result = await this.prefetchPackage(pkg);
2009
+ results.push(result);
2010
+ }
2011
+ const successful = results.filter((r) => r.success).length;
2012
+ const failed = results.filter((r) => !r.success).length;
2013
+ return {
2014
+ totalPackages: packages.length,
2015
+ successful,
2016
+ failed,
2017
+ results
2018
+ };
1962
2019
  } catch (error) {
1963
- if (isObject(error) && "code" in error && error.code === "ENOENT") return null;
1964
- throw new Error(`Failed to read runtime state for server '${serverId}' from '${filePath}': ${toErrorMessage$2(error)}`);
2020
+ throw new Error(`Failed to prefetch packages: ${error instanceof Error ? error.message : String(error)}`);
1965
2021
  }
1966
2022
  }
1967
2023
  /**
1968
- * List all persisted runtime records.
1969
- * @returns Array of runtime records
2024
+ * Prefetch a single package
2025
+ * @param pkg - Package info to prefetch
2026
+ * @returns Result of the prefetch operation
1970
2027
  */
1971
- async list() {
2028
+ async prefetchPackage(pkg) {
1972
2029
  try {
1973
- const files = (await (0, node_fs_promises.readdir)(this.runtimeDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(RUNTIME_FILE_SUFFIX));
1974
- return (await Promise.all(files.map(async (file) => {
1975
- try {
1976
- const content = await (0, node_fs_promises.readFile)((0, node_path.join)(this.runtimeDir, file.name), "utf-8");
1977
- const parsed = JSON.parse(content);
1978
- return isRuntimeStateRecord(parsed) ? parsed : null;
1979
- } catch (error) {
1980
- this.logger.debug(`Skipping unreadable runtime state file ${file.name}`, error);
1981
- return null;
1982
- }
1983
- }))).filter((record) => record !== null);
2030
+ const [command, ...args] = pkg.fullCommand;
2031
+ const result = await this.runCommand(command, args);
2032
+ return {
2033
+ package: pkg,
2034
+ success: result.success,
2035
+ output: result.output
2036
+ };
1984
2037
  } catch (error) {
1985
- if (isObject(error) && "code" in error && error.code === "ENOENT") return [];
1986
- throw new Error(`Failed to list runtime states from '${this.runtimeDir}': ${toErrorMessage$2(error)}`);
2038
+ return {
2039
+ package: pkg,
2040
+ success: false,
2041
+ output: error instanceof Error ? error.message : String(error)
2042
+ };
1987
2043
  }
1988
2044
  }
1989
2045
  /**
1990
- * Remove a runtime state record by server ID.
1991
- * @param serverId - Target mcp-proxy server identifier
1992
- * @returns Promise that resolves when delete completes
2046
+ * Validate package name to prevent command injection
2047
+ * @param packageName - Package name to validate
2048
+ * @returns True if package name is safe, false otherwise
2049
+ * @remarks Rejects package names containing shell metacharacters
2050
+ * @example
2051
+ * isValidPackageName('@scope/package') // true
2052
+ * isValidPackageName('my-package@1.0.0') // true
2053
+ * isValidPackageName('pkg; rm -rf /') // false (shell injection)
2054
+ * isValidPackageName('pkg$(whoami)') // false (command substitution)
1993
2055
  */
1994
- async remove(serverId) {
1995
- await (0, node_fs_promises.rm)(this.getRecordPath(serverId), { force: true });
2056
+ isValidPackageName(packageName) {
2057
+ return VALID_PACKAGE_NAME_PATTERN.test(packageName);
2058
+ }
2059
+ /**
2060
+ * Extract package info from a server's stdio config
2061
+ * @param serverName - Name of the MCP server
2062
+ * @param config - Stdio configuration for the server
2063
+ * @returns Package info if extractable, null otherwise
2064
+ */
2065
+ extractPackageInfo(serverName, config) {
2066
+ const command = config.command.toLowerCase();
2067
+ const args = config.args || [];
2068
+ if (command === "npx" || command.endsWith("/npx")) {
2069
+ const packageName = this.extractNpxPackage(args);
2070
+ if (packageName && this.isValidPackageName(packageName)) return {
2071
+ serverName,
2072
+ packageManager: "npx",
2073
+ packageName,
2074
+ fullCommand: [
2075
+ "npm",
2076
+ ARG_INSTALL,
2077
+ "-g",
2078
+ packageName
2079
+ ]
2080
+ };
2081
+ }
2082
+ if (command === "pnpx" || command.endsWith("/pnpx")) {
2083
+ const packageName = this.extractNpxPackage(args);
2084
+ if (packageName && this.isValidPackageName(packageName)) return {
2085
+ serverName,
2086
+ packageManager: COMMAND_PNPX,
2087
+ packageName,
2088
+ fullCommand: [
2089
+ COMMAND_PNPM,
2090
+ "add",
2091
+ "-g",
2092
+ packageName
2093
+ ]
2094
+ };
2095
+ }
2096
+ if (command === "uvx" || command.endsWith("/uvx")) {
2097
+ const packageName = this.extractUvxPackage(args);
2098
+ if (packageName && this.isValidPackageName(packageName)) return {
2099
+ serverName,
2100
+ packageManager: "uvx",
2101
+ packageName,
2102
+ fullCommand: ["uvx", packageName]
2103
+ };
2104
+ }
2105
+ if ((command === "uv" || command.endsWith("/uv")) && args.includes("run")) {
2106
+ const packageName = this.extractUvRunPackage(args);
2107
+ if (packageName && this.isValidPackageName(packageName)) return {
2108
+ serverName,
2109
+ packageManager: "uv",
2110
+ packageName,
2111
+ fullCommand: [
2112
+ "uv",
2113
+ ARG_TOOL,
2114
+ ARG_INSTALL,
2115
+ packageName
2116
+ ]
2117
+ };
2118
+ }
2119
+ return null;
2120
+ }
2121
+ /**
2122
+ * Extract package name from npx command args
2123
+ * @param args - Command arguments
2124
+ * @returns Package name or null
2125
+ * @remarks Handles --package=value, --package value, -p value patterns.
2126
+ * Falls back to first non-flag argument if no --package/-p flag found.
2127
+ * Returns null if flag has no value or is followed by another flag.
2128
+ * When multiple --package flags exist, returns the first valid one.
2129
+ * @example
2130
+ * extractNpxPackage(['--package=@scope/pkg']) // returns '@scope/pkg'
2131
+ * extractNpxPackage(['--package', 'pkg-name']) // returns 'pkg-name'
2132
+ * extractNpxPackage(['-p', 'pkg']) // returns 'pkg'
2133
+ * extractNpxPackage(['-y', 'pkg-name', '--flag']) // returns 'pkg-name' (fallback)
2134
+ * extractNpxPackage(['--package=']) // returns null (empty value)
2135
+ */
2136
+ extractNpxPackage(args) {
2137
+ for (let i = 0; i < args.length; i++) {
2138
+ const arg = args[i];
2139
+ if (arg.startsWith("--package=")) return arg.slice(10) || null;
2140
+ if (arg === "--package" && i + 1 < args.length) {
2141
+ const nextArg = args[i + 1];
2142
+ if (!nextArg.startsWith("-")) return nextArg;
2143
+ }
2144
+ if (arg === "-p" && i + 1 < args.length) {
2145
+ const nextArg = args[i + 1];
2146
+ if (!nextArg.startsWith("-")) return nextArg;
2147
+ }
2148
+ }
2149
+ for (const arg of args) {
2150
+ if (arg.startsWith("-")) continue;
2151
+ return arg;
2152
+ }
2153
+ return null;
2154
+ }
2155
+ /**
2156
+ * Extract package name from uvx command args
2157
+ * @param args - Command arguments
2158
+ * @returns Package name or null
2159
+ * @remarks Assumes the first non-flag argument is the package name.
2160
+ * Handles both single (-) and double (--) dash flags.
2161
+ * @example
2162
+ * extractUvxPackage(['mcp-server-fetch']) // returns 'mcp-server-fetch'
2163
+ * extractUvxPackage(['--quiet', 'pkg-name']) // returns 'pkg-name'
2164
+ */
2165
+ extractUvxPackage(args) {
2166
+ for (const arg of args) {
2167
+ if (arg.startsWith("-")) continue;
2168
+ return arg;
2169
+ }
2170
+ return null;
2171
+ }
2172
+ /**
2173
+ * Extract package name from uv run command args
2174
+ * @param args - Command arguments
2175
+ * @returns Package name or null
2176
+ * @remarks Looks for the first non-flag argument after the 'run' subcommand.
2177
+ * Returns null if 'run' is not found in args.
2178
+ * @example
2179
+ * extractUvRunPackage(['run', 'mcp-server']) // returns 'mcp-server'
2180
+ * extractUvRunPackage(['run', '--verbose', 'pkg']) // returns 'pkg'
2181
+ * extractUvRunPackage(['install', 'pkg']) // returns null (no 'run')
2182
+ */
2183
+ extractUvRunPackage(args) {
2184
+ const runIndex = args.indexOf("run");
2185
+ if (runIndex === -1) return null;
2186
+ for (let i = runIndex + 1; i < args.length; i++) {
2187
+ const arg = args[i];
2188
+ if (arg.startsWith("-")) continue;
2189
+ return arg;
2190
+ }
2191
+ return null;
2192
+ }
2193
+ /**
2194
+ * Run a shell command and capture output
2195
+ * @param command - Command to run
2196
+ * @param args - Command arguments
2197
+ * @returns Promise with success status and output
2198
+ */
2199
+ runCommand(command, args) {
2200
+ return new Promise((resolve) => {
2201
+ const proc = (0, node_child_process.spawn)(command, args, {
2202
+ stdio: [
2203
+ STDIO_IGNORE,
2204
+ STDIO_PIPE,
2205
+ STDIO_PIPE
2206
+ ],
2207
+ shell: process.platform === PLATFORM_WIN32
2208
+ });
2209
+ let stdout = "";
2210
+ let stderr = "";
2211
+ proc.stdout?.on("data", (data) => {
2212
+ stdout += data.toString();
2213
+ });
2214
+ proc.stderr?.on("data", (data) => {
2215
+ stderr += data.toString();
2216
+ });
2217
+ proc.on("close", (code) => {
2218
+ resolve({
2219
+ success: code === 0,
2220
+ output: stdout || stderr
2221
+ });
2222
+ });
2223
+ proc.on("error", (error) => {
2224
+ resolve({
2225
+ success: false,
2226
+ output: error.message
2227
+ });
2228
+ });
2229
+ });
1996
2230
  }
1997
2231
  };
1998
- /** Path for the runtime health check endpoint. */
1999
- const HEALTH_CHECK_PATH = "/health";
2000
- /** Path for the authenticated admin shutdown endpoint. */
2001
- const ADMIN_SHUTDOWN_PATH = "/admin/shutdown";
2002
- /** HTTP POST method identifier. */
2003
- const HTTP_METHOD_POST = "POST";
2004
- /** HTTP header name for bearer token authorization. */
2005
- const AUTHORIZATION_HEADER_NAME = "Authorization";
2006
- /** Prefix for bearer token values in the Authorization header. */
2007
- const BEARER_TOKEN_PREFIX = "Bearer ";
2008
- /** HTTP protocol scheme prefix for URL construction. */
2009
- const HTTP_PROTOCOL = "http://";
2010
- /** Hosts that are safe to send admin requests to (loopback only). */
2011
- const ALLOWED_HOSTS = new Set([
2012
- "localhost",
2013
- "127.0.0.1",
2014
- "::1"
2015
- ]);
2016
2232
  //#endregion
2017
- //#region src/services/StopServerService/types.ts
2018
- /**
2019
- * Safely cast a non-null object to a string-keyed record for property access.
2020
- * @param value - Object value already verified as non-null
2021
- * @returns The same value typed as a record
2022
- */
2023
- function toRecord(value) {
2024
- return value;
2025
- }
2026
- /**
2027
- * Type guard for health responses.
2028
- * @param value - Candidate payload to validate
2029
- * @returns True when payload matches health response shape
2030
- */
2031
- function isHealthResponse(value) {
2032
- if (typeof value !== "object" || value === null) return false;
2033
- const record = toRecord(value);
2034
- return "status" in record && record["status"] === "ok" && "transport" in record && record["transport"] === "http" && (!("serverId" in record) || record["serverId"] === void 0 || typeof record["serverId"] === "string");
2035
- }
2233
+ //#region src/services/RuntimeStateService.ts
2036
2234
  /**
2037
- * Type guard for shutdown responses.
2038
- * @param value - Candidate payload to validate
2039
- * @returns True when payload matches shutdown response shape
2235
+ * RuntimeStateService
2236
+ *
2237
+ * Persists runtime metadata for HTTP mcp-proxy instances so external commands
2238
+ * (for example `mcp-proxy stop`) can discover and target the correct server.
2040
2239
  */
2041
- function isShutdownResponse(value) {
2042
- if (typeof value !== "object" || value === null) return false;
2043
- const record = toRecord(value);
2044
- return "ok" in record && typeof record["ok"] === "boolean" && "message" in record && typeof record["message"] === "string" && (!("serverId" in record) || record["serverId"] === void 0 || typeof record["serverId"] === "string");
2240
+ const RUNTIME_DIR_NAME = "runtimes";
2241
+ const RUNTIME_FILE_SUFFIX = ".runtime.json";
2242
+ function isObject(value) {
2243
+ return typeof value === "object" && value !== null;
2045
2244
  }
2046
- //#endregion
2047
- //#region src/services/StopServerService/StopServerService.ts
2048
- /**
2049
- * Format runtime endpoint URL after validating the host is a loopback address.
2050
- * Rejects non-loopback hosts to prevent SSRF via tampered runtime state files.
2051
- * @param runtime - Runtime record to format
2052
- * @param path - Request path to append
2053
- * @returns Full runtime URL
2054
- */
2055
- function buildRuntimeUrl(runtime, path) {
2056
- if (!ALLOWED_HOSTS.has(runtime.host)) throw new Error(`Refusing to connect to non-loopback host '${runtime.host}'. Only ${Array.from(ALLOWED_HOSTS).join(", ")} are allowed.`);
2057
- return `${HTTP_PROTOCOL}${runtime.host}:${runtime.port}${path}`;
2245
+ function isRuntimeStateRecord(value) {
2246
+ if (!isObject(value)) return false;
2247
+ return typeof value.serverId === "string" && typeof value.host === "string" && typeof value.port === "number" && value.transport === "http" && typeof value.shutdownToken === "string" && typeof value.startedAt === "string" && typeof value.pid === "number" && (value.configPath === void 0 || typeof value.configPath === "string");
2058
2248
  }
2059
- function toErrorMessage$1(error) {
2249
+ function toErrorMessage$2(error) {
2060
2250
  return error instanceof Error ? error.message : String(error);
2061
2251
  }
2062
- function sleep(delayMs) {
2063
- return new Promise((resolve) => {
2064
- setTimeout(resolve, delayMs);
2065
- });
2066
- }
2067
2252
  /**
2068
- * Service for resolving runtime targets and stopping them safely.
2253
+ * Runtime state persistence implementation.
2069
2254
  */
2070
- var StopServerService = class {
2071
- runtimeStateService;
2255
+ var RuntimeStateService = class RuntimeStateService {
2256
+ runtimeDir;
2072
2257
  logger;
2073
- constructor(runtimeStateService = new RuntimeStateService(), logger = console) {
2074
- this.runtimeStateService = runtimeStateService;
2258
+ constructor(runtimeDir, logger = console) {
2259
+ this.runtimeDir = runtimeDir ?? RuntimeStateService.getDefaultRuntimeDir();
2075
2260
  this.logger = logger;
2076
2261
  }
2077
2262
  /**
2078
- * Resolve a target runtime and stop it cooperatively.
2079
- * @param request - Stop request options
2080
- * @returns Stop result payload
2263
+ * Resolve default runtime directory under the user's home cache path.
2264
+ * @returns Absolute runtime directory path
2081
2265
  */
2082
- async stop(request) {
2083
- const timeoutMs = request.timeoutMs ?? 5e3;
2084
- const runtime = await this.resolveRuntime(request);
2085
- const health = await this.fetchHealth(runtime, timeoutMs);
2086
- if (!health.reachable) {
2087
- await this.runtimeStateService.remove(runtime.serverId);
2088
- throw new Error(`Runtime '${runtime.serverId}' is not reachable at http://${runtime.host}:${runtime.port}. Removed stale runtime record.`);
2089
- }
2090
- if (!request.force && health.payload?.serverId && health.payload.serverId !== runtime.serverId) throw new Error(`Refusing to stop runtime at http://${runtime.host}:${runtime.port}: expected server ID '${runtime.serverId}' but health endpoint reported '${health.payload.serverId}'. Use --force to override.`);
2091
- const shutdownToken = request.token ?? runtime.shutdownToken;
2092
- if (!shutdownToken) throw new Error(`No shutdown token available for runtime '${runtime.serverId}'.`);
2093
- const shutdownResponse = await this.requestShutdown(runtime, shutdownToken, timeoutMs);
2094
- await this.waitForShutdown(runtime, timeoutMs);
2095
- await this.runtimeStateService.remove(runtime.serverId);
2096
- return {
2097
- ok: true,
2098
- serverId: runtime.serverId,
2099
- host: runtime.host,
2100
- port: runtime.port,
2101
- message: shutdownResponse.message
2102
- };
2266
+ static getDefaultRuntimeDir() {
2267
+ return (0, node_path.join)((0, node_os.homedir)(), ".aicode-toolkit", "mcp-proxy", RUNTIME_DIR_NAME);
2103
2268
  }
2104
2269
  /**
2105
- * Resolve a runtime record from explicit ID or a unique host/port pair.
2106
- * @param request - Stop request options
2107
- * @returns Matching runtime record
2270
+ * Build runtime state file path for a given server ID.
2271
+ * @param serverId - Target mcp-proxy server identifier
2272
+ * @returns Absolute runtime file path
2108
2273
  */
2109
- async resolveRuntime(request) {
2110
- if (request.serverId) {
2111
- const runtime = await this.runtimeStateService.read(request.serverId);
2112
- if (!runtime) throw new Error(`No runtime record found for server ID '${request.serverId}'. Start the server with 'mcp-proxy mcp-serve --type http' first.`);
2113
- return runtime;
2114
- }
2115
- if (request.host === void 0 || request.port === void 0) throw new Error("Provide --id or both --host and --port to select a runtime.");
2116
- const matches = (await this.runtimeStateService.list()).filter((runtime) => runtime.host === request.host && runtime.port === request.port);
2117
- if (matches.length === 0) throw new Error(`No runtime record found for http://${request.host}:${request.port}. Start the server with 'mcp-proxy mcp-serve --type http' first.`);
2118
- if (matches.length > 1) throw new Error(`Multiple runtime records match http://${request.host}:${request.port}. Retry with --id to avoid stopping the wrong server.`);
2119
- return matches[0];
2274
+ getRecordPath(serverId) {
2275
+ return (0, node_path.join)(this.runtimeDir, `${serverId}${RUNTIME_FILE_SUFFIX}`);
2120
2276
  }
2121
2277
  /**
2122
- * Read the runtime health payload.
2123
- * @param runtime - Runtime to query
2124
- * @param timeoutMs - Request timeout in milliseconds
2125
- * @returns Reachability status and optional payload
2278
+ * Persist a runtime state record.
2279
+ * @param record - Runtime metadata to persist
2280
+ * @returns Promise that resolves when write completes
2126
2281
  */
2127
- async fetchHealth(runtime, timeoutMs) {
2128
- try {
2129
- const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, HEALTH_CHECK_PATH), { method: "GET" }, timeoutMs);
2130
- if (!response.ok) return { reachable: false };
2131
- const payload = await response.json();
2132
- if (!isHealthResponse(payload)) throw new Error("Received invalid health response payload.");
2133
- return {
2134
- reachable: true,
2135
- payload
2136
- };
2137
- } catch (error) {
2138
- this.logger.debug(`Health check failed for ${runtime.serverId}`, error);
2139
- return { reachable: false };
2140
- }
2282
+ async write(record) {
2283
+ await (0, node_fs_promises.mkdir)(this.runtimeDir, { recursive: true });
2284
+ await (0, node_fs_promises.writeFile)(this.getRecordPath(record.serverId), JSON.stringify(record, null, 2), "utf-8");
2141
2285
  }
2142
2286
  /**
2143
- * Send authenticated shutdown request to the admin endpoint.
2144
- * @param runtime - Runtime to stop
2145
- * @param shutdownToken - Bearer token for the admin endpoint
2146
- * @param timeoutMs - Request timeout in milliseconds
2147
- * @returns Parsed shutdown response payload
2287
+ * Read a runtime state record by server ID.
2288
+ * @param serverId - Target mcp-proxy server identifier
2289
+ * @returns Matching runtime record, or null when no record exists
2148
2290
  */
2149
- async requestShutdown(runtime, shutdownToken, timeoutMs) {
2150
- const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, ADMIN_SHUTDOWN_PATH), {
2151
- method: HTTP_METHOD_POST,
2152
- headers: { [AUTHORIZATION_HEADER_NAME]: `${BEARER_TOKEN_PREFIX}${shutdownToken}` }
2153
- }, timeoutMs);
2154
- const payload = await response.json();
2155
- if (!isShutdownResponse(payload)) throw new Error("Received invalid shutdown response payload.");
2156
- if (!response.ok || !payload.ok) throw new Error(payload.message);
2157
- return payload;
2291
+ async read(serverId) {
2292
+ const filePath = this.getRecordPath(serverId);
2293
+ try {
2294
+ const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
2295
+ const parsed = JSON.parse(content);
2296
+ return isRuntimeStateRecord(parsed) ? parsed : null;
2297
+ } catch (error) {
2298
+ if (isObject(error) && "code" in error && error.code === "ENOENT") return null;
2299
+ throw new Error(`Failed to read runtime state for server '${serverId}' from '${filePath}': ${toErrorMessage$2(error)}`);
2300
+ }
2158
2301
  }
2159
2302
  /**
2160
- * Poll until the target runtime is no longer reachable.
2161
- * @param runtime - Runtime expected to stop
2162
- * @param timeoutMs - Maximum wait time in milliseconds
2163
- * @returns Promise that resolves when shutdown is observed
2303
+ * List all persisted runtime records.
2304
+ * @returns Array of runtime records
2164
2305
  */
2165
- async waitForShutdown(runtime, timeoutMs) {
2166
- const deadline = Date.now() + timeoutMs;
2167
- while (Date.now() < deadline) {
2168
- if (!(await this.fetchHealth(runtime, Math.max(250, deadline - Date.now()))).reachable) return;
2169
- await sleep(200);
2306
+ async list() {
2307
+ try {
2308
+ const files = (await (0, node_fs_promises.readdir)(this.runtimeDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(RUNTIME_FILE_SUFFIX));
2309
+ return (await Promise.all(files.map(async (file) => {
2310
+ try {
2311
+ const content = await (0, node_fs_promises.readFile)((0, node_path.join)(this.runtimeDir, file.name), "utf-8");
2312
+ const parsed = JSON.parse(content);
2313
+ return isRuntimeStateRecord(parsed) ? parsed : null;
2314
+ } catch (error) {
2315
+ this.logger.debug(`Skipping unreadable runtime state file ${file.name}`, error);
2316
+ return null;
2317
+ }
2318
+ }))).filter((record) => record !== null);
2319
+ } catch (error) {
2320
+ if (isObject(error) && "code" in error && error.code === "ENOENT") return [];
2321
+ throw new Error(`Failed to list runtime states from '${this.runtimeDir}': ${toErrorMessage$2(error)}`);
2170
2322
  }
2171
- throw new Error(`Timed out waiting for runtime '${runtime.serverId}' to stop at http://${runtime.host}:${runtime.port}.`);
2172
2323
  }
2173
2324
  /**
2174
- * Perform a fetch with an abort timeout.
2175
- * @param url - Target URL
2176
- * @param init - Fetch options
2177
- * @param timeoutMs - Timeout in milliseconds
2178
- * @returns Fetch response
2325
+ * Remove a runtime state record by server ID.
2326
+ * @param serverId - Target mcp-proxy server identifier
2327
+ * @returns Promise that resolves when delete completes
2179
2328
  */
2180
- async fetchWithTimeout(url, init, timeoutMs) {
2181
- const controller = new AbortController();
2182
- const timeoutId = setTimeout(() => {
2183
- controller.abort();
2184
- }, timeoutMs);
2185
- try {
2186
- return await fetch(url, {
2187
- ...init,
2188
- signal: controller.signal
2189
- });
2190
- } catch (error) {
2191
- throw new Error(`Request to '${url}' failed: ${toErrorMessage$1(error)}`);
2192
- } finally {
2193
- clearTimeout(timeoutId);
2194
- }
2329
+ async remove(serverId) {
2330
+ await (0, node_fs_promises.rm)(this.getRecordPath(serverId), { force: true });
2195
2331
  }
2196
2332
  };
2197
2333
  //#endregion
@@ -2550,339 +2686,203 @@ var SkillService = class {
2550
2686
  };
2551
2687
  }
2552
2688
  };
2553
- /** pnpx command name (pnpm's npx equivalent) */
2554
- const COMMAND_PNPX = "pnpx";
2555
- /** pnpm command name */
2556
- const COMMAND_PNPM = "pnpm";
2557
- /** Tool subcommand for uv */
2558
- const ARG_TOOL = "tool";
2559
- /** Install subcommand for uv tool and npm/pnpm */
2560
- const ARG_INSTALL = "install";
2689
+ /** Path for the runtime health check endpoint. */
2690
+ const HEALTH_CHECK_PATH = "/health";
2691
+ /** Path for the authenticated admin shutdown endpoint. */
2692
+ const ADMIN_SHUTDOWN_PATH = "/admin/shutdown";
2693
+ /** HTTP POST method identifier. */
2694
+ const HTTP_METHOD_POST = "POST";
2695
+ /** HTTP header name for bearer token authorization. */
2696
+ const AUTHORIZATION_HEADER_NAME = "Authorization";
2697
+ /** Prefix for bearer token values in the Authorization header. */
2698
+ const BEARER_TOKEN_PREFIX = "Bearer ";
2699
+ /** HTTP protocol scheme prefix for URL construction. */
2700
+ const HTTP_PROTOCOL = "http://";
2701
+ /** Hosts that are safe to send admin requests to (loopback only). */
2702
+ const ALLOWED_HOSTS = new Set([
2703
+ "localhost",
2704
+ "127.0.0.1",
2705
+ "::1"
2706
+ ]);
2707
+ //#endregion
2708
+ //#region src/services/StopServerService/types.ts
2561
2709
  /**
2562
- * Regex pattern for valid package names (npm, pnpm, uvx, uv)
2563
- * Allows: @scope/package-name@version, package-name, package_name
2564
- * Prevents shell metacharacters that could enable command injection
2565
- * @example
2566
- * // Valid: '@scope/package@1.0.0', 'my-package', 'my_package', '@org/pkg'
2567
- * // Invalid: 'pkg; rm -rf /', 'pkg$(cmd)', 'pkg`whoami`', 'pkg|cat /etc/passwd'
2710
+ * Safely cast a non-null object to a string-keyed record for property access.
2711
+ * @param value - Object value already verified as non-null
2712
+ * @returns The same value typed as a record
2568
2713
  */
2569
- const VALID_PACKAGE_NAME_PATTERN = /^(@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
2570
- /** Windows platform identifier */
2571
- const PLATFORM_WIN32 = "win32";
2572
- /** Stdio option to ignore stream */
2573
- const STDIO_IGNORE = "ignore";
2574
- /** Stdio option to pipe stream */
2575
- const STDIO_PIPE = "pipe";
2576
- //#endregion
2577
- //#region src/services/PrefetchService/PrefetchService.ts
2714
+ function toRecord(value) {
2715
+ return value;
2716
+ }
2578
2717
  /**
2579
- * PrefetchService
2580
- *
2581
- * DESIGN PATTERNS:
2582
- * - Service pattern for business logic encapsulation
2583
- * - Single responsibility principle
2584
- *
2585
- * CODING STANDARDS:
2586
- * - Use async/await for asynchronous operations
2587
- * - Throw descriptive errors for error cases
2588
- * - Keep methods focused and well-named
2589
- * - Document complex logic with comments
2590
- *
2591
- * AVOID:
2592
- * - Mixing concerns (keep focused on single domain)
2593
- * - Direct tool implementation (services should be tool-agnostic)
2718
+ * Type guard for health responses.
2719
+ * @param value - Candidate payload to validate
2720
+ * @returns True when payload matches health response shape
2594
2721
  */
2722
+ function isHealthResponse(value) {
2723
+ if (typeof value !== "object" || value === null) return false;
2724
+ const record = toRecord(value);
2725
+ return "status" in record && record["status"] === "ok" && "transport" in record && record["transport"] === "http" && (!("serverId" in record) || record["serverId"] === void 0 || typeof record["serverId"] === "string");
2726
+ }
2595
2727
  /**
2596
- * Type guard to check if a config object is an McpStdioConfig
2597
- * @param config - Config object to check
2598
- * @returns True if config has required McpStdioConfig properties
2728
+ * Type guard for shutdown responses.
2729
+ * @param value - Candidate payload to validate
2730
+ * @returns True when payload matches shutdown response shape
2599
2731
  */
2600
- function isMcpStdioConfig(config) {
2601
- return typeof config === "object" && config !== null && "command" in config;
2732
+ function isShutdownResponse(value) {
2733
+ if (typeof value !== "object" || value === null) return false;
2734
+ const record = toRecord(value);
2735
+ return "ok" in record && typeof record["ok"] === "boolean" && "message" in record && typeof record["message"] === "string" && (!("serverId" in record) || record["serverId"] === void 0 || typeof record["serverId"] === "string");
2602
2736
  }
2737
+ //#endregion
2738
+ //#region src/services/StopServerService/StopServerService.ts
2603
2739
  /**
2604
- * PrefetchService handles pre-downloading packages used by MCP servers.
2605
- * Supports npx (Node.js), uvx (Python/uv), and uv run commands.
2606
- *
2607
- * @example
2608
- * ```typescript
2609
- * const service = new PrefetchService({
2610
- * mcpConfig: await configFetcher.fetchConfiguration(),
2611
- * parallel: true,
2612
- * });
2613
- * const packages = service.extractPackages();
2614
- * const summary = await service.prefetch();
2615
- * ```
2740
+ * Format runtime endpoint URL after validating the host is a loopback address.
2741
+ * Rejects non-loopback hosts to prevent SSRF via tampered runtime state files.
2742
+ * @param runtime - Runtime record to format
2743
+ * @param path - Request path to append
2744
+ * @returns Full runtime URL
2616
2745
  */
2617
- var PrefetchService = class {
2618
- config;
2619
- /**
2620
- * Creates a new PrefetchService instance
2621
- * @param config - Service configuration options
2622
- */
2623
- constructor(config) {
2624
- this.config = config;
2625
- }
2626
- /**
2627
- * Extract all prefetchable packages from the MCP configuration
2628
- * @returns Array of package info objects
2629
- */
2630
- extractPackages() {
2631
- const packages = [];
2632
- const { mcpConfig, filter } = this.config;
2633
- for (const [serverName, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
2634
- if (serverConfig.disabled) continue;
2635
- if (serverConfig.transport !== "stdio") continue;
2636
- if (!isMcpStdioConfig(serverConfig.config)) continue;
2637
- const packageInfo = this.extractPackageInfo(serverName, serverConfig.config);
2638
- if (packageInfo) {
2639
- if (filter && packageInfo.packageManager !== filter) continue;
2640
- packages.push(packageInfo);
2641
- }
2642
- }
2643
- return packages;
2644
- }
2645
- /**
2646
- * Prefetch all packages from the configuration
2647
- * @returns Summary of prefetch results
2648
- * @throws Error if prefetch operation fails unexpectedly
2649
- */
2650
- async prefetch() {
2651
- try {
2652
- const packages = this.extractPackages();
2653
- const results = [];
2654
- if (packages.length === 0) return {
2655
- totalPackages: 0,
2656
- successful: 0,
2657
- failed: 0,
2658
- results: []
2659
- };
2660
- if (this.config.parallel) {
2661
- const promises = packages.map(async (pkg) => this.prefetchPackage(pkg));
2662
- results.push(...await Promise.all(promises));
2663
- } else for (const pkg of packages) {
2664
- const result = await this.prefetchPackage(pkg);
2665
- results.push(result);
2666
- }
2667
- const successful = results.filter((r) => r.success).length;
2668
- const failed = results.filter((r) => !r.success).length;
2669
- return {
2670
- totalPackages: packages.length,
2671
- successful,
2672
- failed,
2673
- results
2674
- };
2675
- } catch (error) {
2676
- throw new Error(`Failed to prefetch packages: ${error instanceof Error ? error.message : String(error)}`);
2677
- }
2746
+ function buildRuntimeUrl(runtime, path) {
2747
+ if (!ALLOWED_HOSTS.has(runtime.host)) throw new Error(`Refusing to connect to non-loopback host '${runtime.host}'. Only ${Array.from(ALLOWED_HOSTS).join(", ")} are allowed.`);
2748
+ return `${HTTP_PROTOCOL}${runtime.host}:${runtime.port}${path}`;
2749
+ }
2750
+ function toErrorMessage$1(error) {
2751
+ return error instanceof Error ? error.message : String(error);
2752
+ }
2753
+ function sleep(delayMs) {
2754
+ return new Promise((resolve) => {
2755
+ setTimeout(resolve, delayMs);
2756
+ });
2757
+ }
2758
+ /**
2759
+ * Service for resolving runtime targets and stopping them safely.
2760
+ */
2761
+ var StopServerService = class {
2762
+ runtimeStateService;
2763
+ logger;
2764
+ constructor(runtimeStateService = new RuntimeStateService(), logger = console) {
2765
+ this.runtimeStateService = runtimeStateService;
2766
+ this.logger = logger;
2678
2767
  }
2679
2768
  /**
2680
- * Prefetch a single package
2681
- * @param pkg - Package info to prefetch
2682
- * @returns Result of the prefetch operation
2769
+ * Resolve a target runtime and stop it cooperatively.
2770
+ * @param request - Stop request options
2771
+ * @returns Stop result payload
2683
2772
  */
2684
- async prefetchPackage(pkg) {
2685
- try {
2686
- const [command, ...args] = pkg.fullCommand;
2687
- const result = await this.runCommand(command, args);
2688
- return {
2689
- package: pkg,
2690
- success: result.success,
2691
- output: result.output
2692
- };
2693
- } catch (error) {
2694
- return {
2695
- package: pkg,
2696
- success: false,
2697
- output: error instanceof Error ? error.message : String(error)
2698
- };
2773
+ async stop(request) {
2774
+ const timeoutMs = request.timeoutMs ?? 5e3;
2775
+ const runtime = await this.resolveRuntime(request);
2776
+ const health = await this.fetchHealth(runtime, timeoutMs);
2777
+ if (!health.reachable) {
2778
+ await this.runtimeStateService.remove(runtime.serverId);
2779
+ throw new Error(`Runtime '${runtime.serverId}' is not reachable at http://${runtime.host}:${runtime.port}. Removed stale runtime record.`);
2699
2780
  }
2781
+ if (!request.force && health.payload?.serverId && health.payload.serverId !== runtime.serverId) throw new Error(`Refusing to stop runtime at http://${runtime.host}:${runtime.port}: expected server ID '${runtime.serverId}' but health endpoint reported '${health.payload.serverId}'. Use --force to override.`);
2782
+ const shutdownToken = request.token ?? runtime.shutdownToken;
2783
+ if (!shutdownToken) throw new Error(`No shutdown token available for runtime '${runtime.serverId}'.`);
2784
+ const shutdownResponse = await this.requestShutdown(runtime, shutdownToken, timeoutMs);
2785
+ await this.waitForShutdown(runtime, timeoutMs);
2786
+ await this.runtimeStateService.remove(runtime.serverId);
2787
+ return {
2788
+ ok: true,
2789
+ serverId: runtime.serverId,
2790
+ host: runtime.host,
2791
+ port: runtime.port,
2792
+ message: shutdownResponse.message
2793
+ };
2700
2794
  }
2701
2795
  /**
2702
- * Validate package name to prevent command injection
2703
- * @param packageName - Package name to validate
2704
- * @returns True if package name is safe, false otherwise
2705
- * @remarks Rejects package names containing shell metacharacters
2706
- * @example
2707
- * isValidPackageName('@scope/package') // true
2708
- * isValidPackageName('my-package@1.0.0') // true
2709
- * isValidPackageName('pkg; rm -rf /') // false (shell injection)
2710
- * isValidPackageName('pkg$(whoami)') // false (command substitution)
2711
- */
2712
- isValidPackageName(packageName) {
2713
- return VALID_PACKAGE_NAME_PATTERN.test(packageName);
2714
- }
2715
- /**
2716
- * Extract package info from a server's stdio config
2717
- * @param serverName - Name of the MCP server
2718
- * @param config - Stdio configuration for the server
2719
- * @returns Package info if extractable, null otherwise
2796
+ * Resolve a runtime record from explicit ID or a unique host/port pair.
2797
+ * @param request - Stop request options
2798
+ * @returns Matching runtime record
2720
2799
  */
2721
- extractPackageInfo(serverName, config) {
2722
- const command = config.command.toLowerCase();
2723
- const args = config.args || [];
2724
- if (command === "npx" || command.endsWith("/npx")) {
2725
- const packageName = this.extractNpxPackage(args);
2726
- if (packageName && this.isValidPackageName(packageName)) return {
2727
- serverName,
2728
- packageManager: "npx",
2729
- packageName,
2730
- fullCommand: [
2731
- "npm",
2732
- ARG_INSTALL,
2733
- "-g",
2734
- packageName
2735
- ]
2736
- };
2737
- }
2738
- if (command === "pnpx" || command.endsWith("/pnpx")) {
2739
- const packageName = this.extractNpxPackage(args);
2740
- if (packageName && this.isValidPackageName(packageName)) return {
2741
- serverName,
2742
- packageManager: COMMAND_PNPX,
2743
- packageName,
2744
- fullCommand: [
2745
- COMMAND_PNPM,
2746
- "add",
2747
- "-g",
2748
- packageName
2749
- ]
2750
- };
2751
- }
2752
- if (command === "uvx" || command.endsWith("/uvx")) {
2753
- const packageName = this.extractUvxPackage(args);
2754
- if (packageName && this.isValidPackageName(packageName)) return {
2755
- serverName,
2756
- packageManager: "uvx",
2757
- packageName,
2758
- fullCommand: ["uvx", packageName]
2759
- };
2760
- }
2761
- if ((command === "uv" || command.endsWith("/uv")) && args.includes("run")) {
2762
- const packageName = this.extractUvRunPackage(args);
2763
- if (packageName && this.isValidPackageName(packageName)) return {
2764
- serverName,
2765
- packageManager: "uv",
2766
- packageName,
2767
- fullCommand: [
2768
- "uv",
2769
- ARG_TOOL,
2770
- ARG_INSTALL,
2771
- packageName
2772
- ]
2773
- };
2800
+ async resolveRuntime(request) {
2801
+ if (request.serverId) {
2802
+ const runtime = await this.runtimeStateService.read(request.serverId);
2803
+ if (!runtime) throw new Error(`No runtime record found for server ID '${request.serverId}'. Start the server with 'mcp-proxy mcp-serve --type http' first.`);
2804
+ return runtime;
2774
2805
  }
2775
- return null;
2806
+ if (request.host === void 0 || request.port === void 0) throw new Error("Provide --id or both --host and --port to select a runtime.");
2807
+ const matches = (await this.runtimeStateService.list()).filter((runtime) => runtime.host === request.host && runtime.port === request.port);
2808
+ if (matches.length === 0) throw new Error(`No runtime record found for http://${request.host}:${request.port}. Start the server with 'mcp-proxy mcp-serve --type http' first.`);
2809
+ if (matches.length > 1) throw new Error(`Multiple runtime records match http://${request.host}:${request.port}. Retry with --id to avoid stopping the wrong server.`);
2810
+ return matches[0];
2776
2811
  }
2777
2812
  /**
2778
- * Extract package name from npx command args
2779
- * @param args - Command arguments
2780
- * @returns Package name or null
2781
- * @remarks Handles --package=value, --package value, -p value patterns.
2782
- * Falls back to first non-flag argument if no --package/-p flag found.
2783
- * Returns null if flag has no value or is followed by another flag.
2784
- * When multiple --package flags exist, returns the first valid one.
2785
- * @example
2786
- * extractNpxPackage(['--package=@scope/pkg']) // returns '@scope/pkg'
2787
- * extractNpxPackage(['--package', 'pkg-name']) // returns 'pkg-name'
2788
- * extractNpxPackage(['-p', 'pkg']) // returns 'pkg'
2789
- * extractNpxPackage(['-y', 'pkg-name', '--flag']) // returns 'pkg-name' (fallback)
2790
- * extractNpxPackage(['--package=']) // returns null (empty value)
2813
+ * Read the runtime health payload.
2814
+ * @param runtime - Runtime to query
2815
+ * @param timeoutMs - Request timeout in milliseconds
2816
+ * @returns Reachability status and optional payload
2791
2817
  */
2792
- extractNpxPackage(args) {
2793
- for (let i = 0; i < args.length; i++) {
2794
- const arg = args[i];
2795
- if (arg.startsWith("--package=")) return arg.slice(10) || null;
2796
- if (arg === "--package" && i + 1 < args.length) {
2797
- const nextArg = args[i + 1];
2798
- if (!nextArg.startsWith("-")) return nextArg;
2799
- }
2800
- if (arg === "-p" && i + 1 < args.length) {
2801
- const nextArg = args[i + 1];
2802
- if (!nextArg.startsWith("-")) return nextArg;
2803
- }
2804
- }
2805
- for (const arg of args) {
2806
- if (arg.startsWith("-")) continue;
2807
- return arg;
2818
+ async fetchHealth(runtime, timeoutMs) {
2819
+ try {
2820
+ const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, HEALTH_CHECK_PATH), { method: "GET" }, timeoutMs);
2821
+ if (!response.ok) return { reachable: false };
2822
+ const payload = await response.json();
2823
+ if (!isHealthResponse(payload)) throw new Error("Received invalid health response payload.");
2824
+ return {
2825
+ reachable: true,
2826
+ payload
2827
+ };
2828
+ } catch (error) {
2829
+ this.logger.debug(`Health check failed for ${runtime.serverId}`, error);
2830
+ return { reachable: false };
2808
2831
  }
2809
- return null;
2810
2832
  }
2811
2833
  /**
2812
- * Extract package name from uvx command args
2813
- * @param args - Command arguments
2814
- * @returns Package name or null
2815
- * @remarks Assumes the first non-flag argument is the package name.
2816
- * Handles both single (-) and double (--) dash flags.
2817
- * @example
2818
- * extractUvxPackage(['mcp-server-fetch']) // returns 'mcp-server-fetch'
2819
- * extractUvxPackage(['--quiet', 'pkg-name']) // returns 'pkg-name'
2834
+ * Send authenticated shutdown request to the admin endpoint.
2835
+ * @param runtime - Runtime to stop
2836
+ * @param shutdownToken - Bearer token for the admin endpoint
2837
+ * @param timeoutMs - Request timeout in milliseconds
2838
+ * @returns Parsed shutdown response payload
2820
2839
  */
2821
- extractUvxPackage(args) {
2822
- for (const arg of args) {
2823
- if (arg.startsWith("-")) continue;
2824
- return arg;
2825
- }
2826
- return null;
2840
+ async requestShutdown(runtime, shutdownToken, timeoutMs) {
2841
+ const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, ADMIN_SHUTDOWN_PATH), {
2842
+ method: HTTP_METHOD_POST,
2843
+ headers: { [AUTHORIZATION_HEADER_NAME]: `${BEARER_TOKEN_PREFIX}${shutdownToken}` }
2844
+ }, timeoutMs);
2845
+ const payload = await response.json();
2846
+ if (!isShutdownResponse(payload)) throw new Error("Received invalid shutdown response payload.");
2847
+ if (!response.ok || !payload.ok) throw new Error(payload.message);
2848
+ return payload;
2827
2849
  }
2828
2850
  /**
2829
- * Extract package name from uv run command args
2830
- * @param args - Command arguments
2831
- * @returns Package name or null
2832
- * @remarks Looks for the first non-flag argument after the 'run' subcommand.
2833
- * Returns null if 'run' is not found in args.
2834
- * @example
2835
- * extractUvRunPackage(['run', 'mcp-server']) // returns 'mcp-server'
2836
- * extractUvRunPackage(['run', '--verbose', 'pkg']) // returns 'pkg'
2837
- * extractUvRunPackage(['install', 'pkg']) // returns null (no 'run')
2851
+ * Poll until the target runtime is no longer reachable.
2852
+ * @param runtime - Runtime expected to stop
2853
+ * @param timeoutMs - Maximum wait time in milliseconds
2854
+ * @returns Promise that resolves when shutdown is observed
2838
2855
  */
2839
- extractUvRunPackage(args) {
2840
- const runIndex = args.indexOf("run");
2841
- if (runIndex === -1) return null;
2842
- for (let i = runIndex + 1; i < args.length; i++) {
2843
- const arg = args[i];
2844
- if (arg.startsWith("-")) continue;
2845
- return arg;
2856
+ async waitForShutdown(runtime, timeoutMs) {
2857
+ const deadline = Date.now() + timeoutMs;
2858
+ while (Date.now() < deadline) {
2859
+ if (!(await this.fetchHealth(runtime, Math.max(250, deadline - Date.now()))).reachable) return;
2860
+ await sleep(200);
2846
2861
  }
2847
- return null;
2862
+ throw new Error(`Timed out waiting for runtime '${runtime.serverId}' to stop at http://${runtime.host}:${runtime.port}.`);
2848
2863
  }
2849
2864
  /**
2850
- * Run a shell command and capture output
2851
- * @param command - Command to run
2852
- * @param args - Command arguments
2853
- * @returns Promise with success status and output
2865
+ * Perform a fetch with an abort timeout.
2866
+ * @param url - Target URL
2867
+ * @param init - Fetch options
2868
+ * @param timeoutMs - Timeout in milliseconds
2869
+ * @returns Fetch response
2854
2870
  */
2855
- runCommand(command, args) {
2856
- return new Promise((resolve) => {
2857
- const proc = (0, node_child_process.spawn)(command, args, {
2858
- stdio: [
2859
- STDIO_IGNORE,
2860
- STDIO_PIPE,
2861
- STDIO_PIPE
2862
- ],
2863
- shell: process.platform === PLATFORM_WIN32
2864
- });
2865
- let stdout = "";
2866
- let stderr = "";
2867
- proc.stdout?.on("data", (data) => {
2868
- stdout += data.toString();
2869
- });
2870
- proc.stderr?.on("data", (data) => {
2871
- stderr += data.toString();
2872
- });
2873
- proc.on("close", (code) => {
2874
- resolve({
2875
- success: code === 0,
2876
- output: stdout || stderr
2877
- });
2878
- });
2879
- proc.on("error", (error) => {
2880
- resolve({
2881
- success: false,
2882
- output: error.message
2883
- });
2871
+ async fetchWithTimeout(url, init, timeoutMs) {
2872
+ const controller = new AbortController();
2873
+ const timeoutId = setTimeout(() => {
2874
+ controller.abort();
2875
+ }, timeoutMs);
2876
+ try {
2877
+ return await fetch(url, {
2878
+ ...init,
2879
+ signal: controller.signal
2884
2880
  });
2885
- });
2881
+ } catch (error) {
2882
+ throw new Error(`Request to '${url}' failed: ${toErrorMessage$1(error)}`);
2883
+ } finally {
2884
+ clearTimeout(timeoutId);
2885
+ }
2886
2886
  }
2887
2887
  };
2888
2888
  //#endregion
@@ -2929,8 +2929,6 @@ var DescribeToolsTool = class DescribeToolsTool {
2929
2929
  skillService;
2930
2930
  definitionsCacheService;
2931
2931
  liquid = new liquidjs.Liquid();
2932
- /** Cache for auto-detected skills from prompt front-matter */
2933
- autoDetectedSkillsCache = null;
2934
2932
  /** Unique server identifier for this mcp-proxy instance */
2935
2933
  serverId;
2936
2934
  /**
@@ -2951,67 +2949,9 @@ var DescribeToolsTool = class DescribeToolsTool {
2951
2949
  * the skill service cache is invalidated.
2952
2950
  */
2953
2951
  clearAutoDetectedSkillsCache() {
2954
- this.autoDetectedSkillsCache = null;
2955
2952
  this.definitionsCacheService.clearLiveCache();
2956
2953
  }
2957
2954
  /**
2958
- * Detects and caches skills from prompt front-matter across all connected MCP servers.
2959
- * Fetches all prompts and checks their content for YAML front-matter with name/description.
2960
- * Results are cached to avoid repeated fetches.
2961
- *
2962
- * Error Handling Strategy:
2963
- * - Errors are logged to stderr but do not fail the overall detection process
2964
- * - This ensures partial results are returned even if some servers/prompts fail
2965
- * - Common failure reasons: server temporarily unavailable, prompt requires arguments,
2966
- * network timeout, or server doesn't support listPrompts
2967
- * - Errors are prefixed with [skill-detection] for easy filtering in logs
2968
- *
2969
- * @returns Array of auto-detected skills from prompt front-matter
2970
- */
2971
- async detectSkillsFromPromptFrontMatter() {
2972
- if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
2973
- const clients = this.clientManager.getAllClients();
2974
- let listPromptsFailures = 0;
2975
- let fetchPromptFailures = 0;
2976
- const autoDetectedSkills = (await Promise.all(clients.map(async (client) => {
2977
- const detectedSkills = [];
2978
- const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
2979
- try {
2980
- const prompts = await client.listPrompts();
2981
- if (!prompts || prompts.length === 0) return detectedSkills;
2982
- const promptResults = await Promise.all(prompts.map(async (promptInfo) => {
2983
- if (configuredPromptNames.has(promptInfo.name)) return null;
2984
- try {
2985
- const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
2986
- const content = m.content;
2987
- if (typeof content === "string") return content;
2988
- if (content && typeof content === "object" && "text" in content) return String(content.text);
2989
- return "";
2990
- }).join("\n"));
2991
- if (skillExtraction) return {
2992
- serverName: client.serverName,
2993
- promptName: promptInfo.name,
2994
- skill: skillExtraction.skill
2995
- };
2996
- return null;
2997
- } catch (error) {
2998
- fetchPromptFailures++;
2999
- console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
3000
- return null;
3001
- }
3002
- }));
3003
- for (const result of promptResults) if (result) detectedSkills.push(result);
3004
- } catch (error) {
3005
- listPromptsFailures++;
3006
- console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
3007
- }
3008
- return detectedSkills;
3009
- }))).flat();
3010
- if (listPromptsFailures > 0 || fetchPromptFailures > 0) console.error(`${LOG_PREFIX_SKILL_DETECTION} Completed with ${listPromptsFailures} server failure(s) and ${fetchPromptFailures} prompt failure(s). Detected ${autoDetectedSkills.length} skill(s).`);
3011
- this.autoDetectedSkillsCache = autoDetectedSkills;
3012
- return autoDetectedSkills;
3013
- }
3014
- /**
3015
2955
  * Collects skills derived from prompt configurations across all connected MCP servers.
3016
2956
  * Includes both explicitly configured prompts and auto-detected skills from front-matter.
3017
2957
  *
@@ -3220,7 +3160,20 @@ var DescribeToolsTool = class DescribeToolsTool {
3220
3160
  if (serverName) {
3221
3161
  const serverTools = serverToolsMap.get(serverName);
3222
3162
  if (!serverTools) {
3223
- result.notFound = requestedName;
3163
+ try {
3164
+ const tool = (await (await this.clientManager.ensureConnected(serverName)).listTools()).find((t) => t.name === actualToolName);
3165
+ if (tool) result.tools.push({
3166
+ server: serverName,
3167
+ tool: {
3168
+ name: tool.name,
3169
+ description: tool.description,
3170
+ inputSchema: tool.inputSchema
3171
+ }
3172
+ });
3173
+ else result.notFound = requestedName;
3174
+ } catch {
3175
+ result.notFound = requestedName;
3176
+ }
3224
3177
  return result;
3225
3178
  }
3226
3179
  const tool = serverTools.find((t) => t.name === actualToolName);