@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.
- package/dist/cli.cjs +1208 -1202
- package/dist/cli.mjs +1208 -1202
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -18
- package/dist/index.d.mts +4 -21
- package/dist/index.mjs +1 -1
- package/dist/{src-Dorvm5bM.mjs → src-Dv7rJN0P.mjs} +631 -678
- package/dist/{src-dZuRf3Wt.cjs → src-ElP1ds81.cjs} +630 -677
- package/package.json +5 -5
|
@@ -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.
|
|
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/
|
|
969
|
+
//#region src/utils/generateServerId.ts
|
|
970
970
|
/**
|
|
971
|
-
*
|
|
972
|
-
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
971
|
+
* generateServerId Utilities
|
|
973
972
|
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
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
|
-
*
|
|
979
|
-
*
|
|
1013
|
+
* generateServerId() // "abc234"
|
|
1014
|
+
* generateServerId(4) // "x7mn"
|
|
980
1015
|
*/
|
|
981
|
-
function
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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/
|
|
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
|
-
*
|
|
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
|
|
1164
|
-
* @returns
|
|
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
|
-
*
|
|
1168
|
-
*
|
|
1175
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1176
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1169
1177
|
*/
|
|
1170
|
-
function
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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/
|
|
1921
|
+
//#region src/services/PrefetchService/PrefetchService.ts
|
|
1899
1922
|
/**
|
|
1900
|
-
*
|
|
1923
|
+
* PrefetchService
|
|
1901
1924
|
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
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
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
function
|
|
1911
|
-
|
|
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
|
-
*
|
|
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
|
|
1921
|
-
|
|
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
|
-
*
|
|
1936
|
-
* @param
|
|
1937
|
-
* @returns Absolute runtime file path
|
|
1964
|
+
* Creates a new PrefetchService instance
|
|
1965
|
+
* @param config - Service configuration options
|
|
1938
1966
|
*/
|
|
1939
|
-
|
|
1940
|
-
|
|
1967
|
+
constructor(config) {
|
|
1968
|
+
this.config = config;
|
|
1941
1969
|
}
|
|
1942
1970
|
/**
|
|
1943
|
-
*
|
|
1944
|
-
* @
|
|
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
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
-
*
|
|
1953
|
-
* @
|
|
1954
|
-
* @
|
|
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
|
|
1957
|
-
const filePath = this.getRecordPath(serverId);
|
|
1994
|
+
async prefetch() {
|
|
1958
1995
|
try {
|
|
1959
|
-
const
|
|
1960
|
-
const
|
|
1961
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1969
|
-
* @
|
|
2024
|
+
* Prefetch a single package
|
|
2025
|
+
* @param pkg - Package info to prefetch
|
|
2026
|
+
* @returns Result of the prefetch operation
|
|
1970
2027
|
*/
|
|
1971
|
-
async
|
|
2028
|
+
async prefetchPackage(pkg) {
|
|
1972
2029
|
try {
|
|
1973
|
-
const
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
-
|
|
1986
|
-
|
|
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
|
-
*
|
|
1991
|
-
* @param
|
|
1992
|
-
* @returns
|
|
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
|
-
|
|
1995
|
-
|
|
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/
|
|
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
|
-
*
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
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
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
return
|
|
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
|
-
|
|
2047
|
-
|
|
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$
|
|
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
|
-
*
|
|
2253
|
+
* Runtime state persistence implementation.
|
|
2069
2254
|
*/
|
|
2070
|
-
var
|
|
2071
|
-
|
|
2255
|
+
var RuntimeStateService = class RuntimeStateService {
|
|
2256
|
+
runtimeDir;
|
|
2072
2257
|
logger;
|
|
2073
|
-
constructor(
|
|
2074
|
-
this.
|
|
2258
|
+
constructor(runtimeDir, logger = console) {
|
|
2259
|
+
this.runtimeDir = runtimeDir ?? RuntimeStateService.getDefaultRuntimeDir();
|
|
2075
2260
|
this.logger = logger;
|
|
2076
2261
|
}
|
|
2077
2262
|
/**
|
|
2078
|
-
* Resolve
|
|
2079
|
-
* @
|
|
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
|
-
|
|
2083
|
-
|
|
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
|
-
*
|
|
2106
|
-
* @param
|
|
2107
|
-
* @returns
|
|
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
|
-
|
|
2110
|
-
|
|
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
|
-
*
|
|
2123
|
-
* @param
|
|
2124
|
-
* @
|
|
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
|
|
2128
|
-
|
|
2129
|
-
|
|
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
|
-
*
|
|
2144
|
-
* @param
|
|
2145
|
-
* @
|
|
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
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
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
|
-
*
|
|
2161
|
-
* @
|
|
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
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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
|
-
*
|
|
2175
|
-
* @param
|
|
2176
|
-
* @
|
|
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
|
|
2181
|
-
|
|
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
|
-
/**
|
|
2554
|
-
const
|
|
2555
|
-
/**
|
|
2556
|
-
const
|
|
2557
|
-
/**
|
|
2558
|
-
const
|
|
2559
|
-
/**
|
|
2560
|
-
const
|
|
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
|
-
*
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
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
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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
|
-
*
|
|
2580
|
-
*
|
|
2581
|
-
*
|
|
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
|
|
2597
|
-
* @param
|
|
2598
|
-
* @returns True
|
|
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
|
|
2601
|
-
|
|
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
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2607
|
-
* @
|
|
2608
|
-
*
|
|
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
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
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
|
-
*
|
|
2681
|
-
* @param
|
|
2682
|
-
* @returns
|
|
2769
|
+
* Resolve a target runtime and stop it cooperatively.
|
|
2770
|
+
* @param request - Stop request options
|
|
2771
|
+
* @returns Stop result payload
|
|
2683
2772
|
*/
|
|
2684
|
-
async
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
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
|
-
*
|
|
2703
|
-
* @param
|
|
2704
|
-
* @returns
|
|
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
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2779
|
-
* @param
|
|
2780
|
-
* @
|
|
2781
|
-
* @
|
|
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
|
-
|
|
2793
|
-
|
|
2794
|
-
const
|
|
2795
|
-
if (
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
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
|
-
*
|
|
2813
|
-
* @param
|
|
2814
|
-
* @
|
|
2815
|
-
* @
|
|
2816
|
-
*
|
|
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
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
}
|
|
2826
|
-
|
|
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
|
-
*
|
|
2830
|
-
* @param
|
|
2831
|
-
* @
|
|
2832
|
-
* @
|
|
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
|
-
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
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
|
-
|
|
2862
|
+
throw new Error(`Timed out waiting for runtime '${runtime.serverId}' to stop at http://${runtime.host}:${runtime.port}.`);
|
|
2848
2863
|
}
|
|
2849
2864
|
/**
|
|
2850
|
-
*
|
|
2851
|
-
* @param
|
|
2852
|
-
* @param
|
|
2853
|
-
* @
|
|
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
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
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
|
-
|
|
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);
|