@agimon-ai/mcp-proxy 0.9.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 +1178 -1178
- package/dist/cli.mjs +1178 -1178
- 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-DA5H3rpr.mjs → src-Dv7rJN0P.mjs} +631 -678
- package/dist/{src-DJJH7z8i.cjs → src-ElP1ds81.cjs} +630 -677
- package/package.json +2 -2
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { coerceArgs, formatZodError, jsonSchemaToZod } from "@agimon-ai/foundation-validator";
|
|
2
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
-
import { CallToolRequestSchema, ElicitRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
1
|
import { existsSync } from "node:fs";
|
|
6
2
|
import { access, mkdir, readFile, readdir, rm, stat, unlink, watch, writeFile } from "node:fs/promises";
|
|
7
3
|
import yaml from "js-yaml";
|
|
4
|
+
import { z } from "zod";
|
|
8
5
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
6
|
import { homedir, tmpdir } from "node:os";
|
|
10
7
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
@@ -15,16 +12,19 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
15
12
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
16
13
|
import { spawn } from "node:child_process";
|
|
17
14
|
import { Liquid } from "liquidjs";
|
|
15
|
+
import { coerceArgs, formatZodError, jsonSchemaToZod } from "@agimon-ai/foundation-validator";
|
|
18
16
|
import { once } from "node:events";
|
|
19
17
|
import { createServer } from "node:http";
|
|
20
18
|
import { promisify } from "node:util";
|
|
21
19
|
import { getRequestListener } from "@hono/node-server";
|
|
22
20
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
21
|
+
import { CallToolRequestSchema, ElicitRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
23
22
|
import { Hono } from "hono";
|
|
24
23
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
25
24
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
26
26
|
//#region package.json
|
|
27
|
-
var version = "0.
|
|
27
|
+
var version = "0.9.0";
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/utils/mcpConfigSchema.ts
|
|
30
30
|
/**
|
|
@@ -943,25 +943,68 @@ function findConfigFile() {
|
|
|
943
943
|
return null;
|
|
944
944
|
}
|
|
945
945
|
//#endregion
|
|
946
|
-
//#region src/utils/
|
|
946
|
+
//#region src/utils/generateServerId.ts
|
|
947
947
|
/**
|
|
948
|
-
*
|
|
949
|
-
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
948
|
+
* generateServerId Utilities
|
|
950
949
|
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
950
|
+
* DESIGN PATTERNS:
|
|
951
|
+
* - Pure functions with no side effects
|
|
952
|
+
* - Single responsibility per function
|
|
953
|
+
* - Functional programming approach
|
|
954
|
+
*
|
|
955
|
+
* CODING STANDARDS:
|
|
956
|
+
* - Export individual functions, not classes
|
|
957
|
+
* - Use descriptive function names with verbs
|
|
958
|
+
* - Add JSDoc comments for complex logic
|
|
959
|
+
* - Keep functions small and focused
|
|
960
|
+
*
|
|
961
|
+
* AVOID:
|
|
962
|
+
* - Side effects (mutating external state)
|
|
963
|
+
* - Stateful logic (use services for state)
|
|
964
|
+
* - Complex external dependencies
|
|
965
|
+
*/
|
|
966
|
+
/**
|
|
967
|
+
* Character set for generating human-readable IDs.
|
|
968
|
+
* Excludes confusing characters: 0, O, 1, l, I
|
|
969
|
+
*/
|
|
970
|
+
const CHARSET = "23456789abcdefghjkmnpqrstuvwxyz";
|
|
971
|
+
/**
|
|
972
|
+
* Default length for generated server IDs (6 characters)
|
|
973
|
+
*/
|
|
974
|
+
const DEFAULT_ID_LENGTH = 6;
|
|
975
|
+
/**
|
|
976
|
+
* Generate a short, human-readable server ID.
|
|
977
|
+
*
|
|
978
|
+
* Uses Node.js crypto.randomBytes for cryptographically secure randomness
|
|
979
|
+
* with rejection sampling to avoid modulo bias.
|
|
980
|
+
*
|
|
981
|
+
* The generated ID:
|
|
982
|
+
* - Is 6 characters long by default
|
|
983
|
+
* - Uses only lowercase alphanumeric characters
|
|
984
|
+
* - Excludes confusing characters (0, O, 1, l, I)
|
|
985
|
+
*
|
|
986
|
+
* @param length - Length of the ID to generate (default: 6)
|
|
987
|
+
* @returns A random, human-readable ID
|
|
953
988
|
*
|
|
954
989
|
* @example
|
|
955
|
-
*
|
|
956
|
-
*
|
|
990
|
+
* generateServerId() // "abc234"
|
|
991
|
+
* generateServerId(4) // "x7mn"
|
|
957
992
|
*/
|
|
958
|
-
function
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
993
|
+
function generateServerId(length = DEFAULT_ID_LENGTH) {
|
|
994
|
+
const charsetLength = 31;
|
|
995
|
+
const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength - 1;
|
|
996
|
+
let result = "";
|
|
997
|
+
let remaining = length;
|
|
998
|
+
while (remaining > 0) {
|
|
999
|
+
const bytes = randomBytes(remaining);
|
|
1000
|
+
for (let i = 0; i < bytes.length && remaining > 0; i++) {
|
|
1001
|
+
const byte = bytes[i];
|
|
1002
|
+
if (byte > maxUnbiased) continue;
|
|
1003
|
+
result += CHARSET[byte % charsetLength];
|
|
1004
|
+
remaining--;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return result;
|
|
965
1008
|
}
|
|
966
1009
|
//#endregion
|
|
967
1010
|
//#region src/utils/parseFrontMatter.ts
|
|
@@ -1097,68 +1140,25 @@ function extractSkillFrontMatter(content) {
|
|
|
1097
1140
|
return null;
|
|
1098
1141
|
}
|
|
1099
1142
|
//#endregion
|
|
1100
|
-
//#region src/utils/
|
|
1101
|
-
/**
|
|
1102
|
-
* generateServerId Utilities
|
|
1103
|
-
*
|
|
1104
|
-
* DESIGN PATTERNS:
|
|
1105
|
-
* - Pure functions with no side effects
|
|
1106
|
-
* - Single responsibility per function
|
|
1107
|
-
* - Functional programming approach
|
|
1108
|
-
*
|
|
1109
|
-
* CODING STANDARDS:
|
|
1110
|
-
* - Export individual functions, not classes
|
|
1111
|
-
* - Use descriptive function names with verbs
|
|
1112
|
-
* - Add JSDoc comments for complex logic
|
|
1113
|
-
* - Keep functions small and focused
|
|
1114
|
-
*
|
|
1115
|
-
* AVOID:
|
|
1116
|
-
* - Side effects (mutating external state)
|
|
1117
|
-
* - Stateful logic (use services for state)
|
|
1118
|
-
* - Complex external dependencies
|
|
1119
|
-
*/
|
|
1120
|
-
/**
|
|
1121
|
-
* Character set for generating human-readable IDs.
|
|
1122
|
-
* Excludes confusing characters: 0, O, 1, l, I
|
|
1123
|
-
*/
|
|
1124
|
-
const CHARSET = "23456789abcdefghjkmnpqrstuvwxyz";
|
|
1125
|
-
/**
|
|
1126
|
-
* Default length for generated server IDs (6 characters)
|
|
1127
|
-
*/
|
|
1128
|
-
const DEFAULT_ID_LENGTH = 6;
|
|
1143
|
+
//#region src/utils/parseToolName.ts
|
|
1129
1144
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
* Uses Node.js crypto.randomBytes for cryptographically secure randomness
|
|
1133
|
-
* with rejection sampling to avoid modulo bias.
|
|
1134
|
-
*
|
|
1135
|
-
* The generated ID:
|
|
1136
|
-
* - Is 6 characters long by default
|
|
1137
|
-
* - Uses only lowercase alphanumeric characters
|
|
1138
|
-
* - Excludes confusing characters (0, O, 1, l, I)
|
|
1145
|
+
* Parse tool name to extract server and actual tool name
|
|
1146
|
+
* Supports both plain tool names and prefixed format: {serverName}__{toolName}
|
|
1139
1147
|
*
|
|
1140
|
-
* @param
|
|
1141
|
-
* @returns
|
|
1148
|
+
* @param toolName - The tool name to parse (e.g., "my_tool" or "server__my_tool")
|
|
1149
|
+
* @returns Parsed result with optional serverName and actualToolName
|
|
1142
1150
|
*
|
|
1143
1151
|
* @example
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1152
|
+
* parseToolName("my_tool") // { actualToolName: "my_tool" }
|
|
1153
|
+
* parseToolName("server__my_tool") // { serverName: "server", actualToolName: "my_tool" }
|
|
1146
1154
|
*/
|
|
1147
|
-
function
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
for (let i = 0; i < bytes.length && remaining > 0; i++) {
|
|
1155
|
-
const byte = bytes[i];
|
|
1156
|
-
if (byte > maxUnbiased) continue;
|
|
1157
|
-
result += CHARSET[byte % charsetLength];
|
|
1158
|
-
remaining--;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
return result;
|
|
1155
|
+
function parseToolName(toolName) {
|
|
1156
|
+
const separatorIndex = toolName.indexOf("__");
|
|
1157
|
+
if (separatorIndex > 0) return {
|
|
1158
|
+
serverName: toolName.substring(0, separatorIndex),
|
|
1159
|
+
actualToolName: toolName.substring(separatorIndex + 2)
|
|
1160
|
+
};
|
|
1161
|
+
return { actualToolName: toolName };
|
|
1162
1162
|
}
|
|
1163
1163
|
//#endregion
|
|
1164
1164
|
//#region src/services/DefinitionsCacheService.ts
|
|
@@ -1871,304 +1871,440 @@ var McpClientManagerService = class {
|
|
|
1871
1871
|
return this.clients.has(serverName);
|
|
1872
1872
|
}
|
|
1873
1873
|
};
|
|
1874
|
+
/** pnpx command name (pnpm's npx equivalent) */
|
|
1875
|
+
const COMMAND_PNPX = "pnpx";
|
|
1876
|
+
/** pnpm command name */
|
|
1877
|
+
const COMMAND_PNPM = "pnpm";
|
|
1878
|
+
/** Tool subcommand for uv */
|
|
1879
|
+
const ARG_TOOL = "tool";
|
|
1880
|
+
/** Install subcommand for uv tool and npm/pnpm */
|
|
1881
|
+
const ARG_INSTALL = "install";
|
|
1882
|
+
/**
|
|
1883
|
+
* Regex pattern for valid package names (npm, pnpm, uvx, uv)
|
|
1884
|
+
* Allows: @scope/package-name@version, package-name, package_name
|
|
1885
|
+
* Prevents shell metacharacters that could enable command injection
|
|
1886
|
+
* @example
|
|
1887
|
+
* // Valid: '@scope/package@1.0.0', 'my-package', 'my_package', '@org/pkg'
|
|
1888
|
+
* // Invalid: 'pkg; rm -rf /', 'pkg$(cmd)', 'pkg`whoami`', 'pkg|cat /etc/passwd'
|
|
1889
|
+
*/
|
|
1890
|
+
const VALID_PACKAGE_NAME_PATTERN = /^(@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
1891
|
+
/** Windows platform identifier */
|
|
1892
|
+
const PLATFORM_WIN32 = "win32";
|
|
1893
|
+
/** Stdio option to ignore stream */
|
|
1894
|
+
const STDIO_IGNORE = "ignore";
|
|
1895
|
+
/** Stdio option to pipe stream */
|
|
1896
|
+
const STDIO_PIPE = "pipe";
|
|
1874
1897
|
//#endregion
|
|
1875
|
-
//#region src/services/
|
|
1898
|
+
//#region src/services/PrefetchService/PrefetchService.ts
|
|
1876
1899
|
/**
|
|
1877
|
-
*
|
|
1900
|
+
* PrefetchService
|
|
1878
1901
|
*
|
|
1879
|
-
*
|
|
1880
|
-
*
|
|
1902
|
+
* DESIGN PATTERNS:
|
|
1903
|
+
* - Service pattern for business logic encapsulation
|
|
1904
|
+
* - Single responsibility principle
|
|
1905
|
+
*
|
|
1906
|
+
* CODING STANDARDS:
|
|
1907
|
+
* - Use async/await for asynchronous operations
|
|
1908
|
+
* - Throw descriptive errors for error cases
|
|
1909
|
+
* - Keep methods focused and well-named
|
|
1910
|
+
* - Document complex logic with comments
|
|
1911
|
+
*
|
|
1912
|
+
* AVOID:
|
|
1913
|
+
* - Mixing concerns (keep focused on single domain)
|
|
1914
|
+
* - Direct tool implementation (services should be tool-agnostic)
|
|
1881
1915
|
*/
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
function
|
|
1888
|
-
|
|
1889
|
-
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");
|
|
1890
|
-
}
|
|
1891
|
-
function toErrorMessage$2(error) {
|
|
1892
|
-
return error instanceof Error ? error.message : String(error);
|
|
1916
|
+
/**
|
|
1917
|
+
* Type guard to check if a config object is an McpStdioConfig
|
|
1918
|
+
* @param config - Config object to check
|
|
1919
|
+
* @returns True if config has required McpStdioConfig properties
|
|
1920
|
+
*/
|
|
1921
|
+
function isMcpStdioConfig(config) {
|
|
1922
|
+
return typeof config === "object" && config !== null && "command" in config;
|
|
1893
1923
|
}
|
|
1894
1924
|
/**
|
|
1895
|
-
*
|
|
1925
|
+
* PrefetchService handles pre-downloading packages used by MCP servers.
|
|
1926
|
+
* Supports npx (Node.js), uvx (Python/uv), and uv run commands.
|
|
1927
|
+
*
|
|
1928
|
+
* @example
|
|
1929
|
+
* ```typescript
|
|
1930
|
+
* const service = new PrefetchService({
|
|
1931
|
+
* mcpConfig: await configFetcher.fetchConfiguration(),
|
|
1932
|
+
* parallel: true,
|
|
1933
|
+
* });
|
|
1934
|
+
* const packages = service.extractPackages();
|
|
1935
|
+
* const summary = await service.prefetch();
|
|
1936
|
+
* ```
|
|
1896
1937
|
*/
|
|
1897
|
-
var
|
|
1898
|
-
|
|
1899
|
-
logger;
|
|
1900
|
-
constructor(runtimeDir, logger = console) {
|
|
1901
|
-
this.runtimeDir = runtimeDir ?? RuntimeStateService.getDefaultRuntimeDir();
|
|
1902
|
-
this.logger = logger;
|
|
1903
|
-
}
|
|
1904
|
-
/**
|
|
1905
|
-
* Resolve default runtime directory under the user's home cache path.
|
|
1906
|
-
* @returns Absolute runtime directory path
|
|
1907
|
-
*/
|
|
1908
|
-
static getDefaultRuntimeDir() {
|
|
1909
|
-
return join(homedir(), ".aicode-toolkit", "mcp-proxy", RUNTIME_DIR_NAME);
|
|
1910
|
-
}
|
|
1938
|
+
var PrefetchService = class {
|
|
1939
|
+
config;
|
|
1911
1940
|
/**
|
|
1912
|
-
*
|
|
1913
|
-
* @param
|
|
1914
|
-
* @returns Absolute runtime file path
|
|
1941
|
+
* Creates a new PrefetchService instance
|
|
1942
|
+
* @param config - Service configuration options
|
|
1915
1943
|
*/
|
|
1916
|
-
|
|
1917
|
-
|
|
1944
|
+
constructor(config) {
|
|
1945
|
+
this.config = config;
|
|
1918
1946
|
}
|
|
1919
1947
|
/**
|
|
1920
|
-
*
|
|
1921
|
-
* @
|
|
1922
|
-
* @returns Promise that resolves when write completes
|
|
1948
|
+
* Extract all prefetchable packages from the MCP configuration
|
|
1949
|
+
* @returns Array of package info objects
|
|
1923
1950
|
*/
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1951
|
+
extractPackages() {
|
|
1952
|
+
const packages = [];
|
|
1953
|
+
const { mcpConfig, filter } = this.config;
|
|
1954
|
+
for (const [serverName, serverConfig] of Object.entries(mcpConfig.mcpServers)) {
|
|
1955
|
+
if (serverConfig.disabled) continue;
|
|
1956
|
+
if (serverConfig.transport !== "stdio") continue;
|
|
1957
|
+
if (!isMcpStdioConfig(serverConfig.config)) continue;
|
|
1958
|
+
const packageInfo = this.extractPackageInfo(serverName, serverConfig.config);
|
|
1959
|
+
if (packageInfo) {
|
|
1960
|
+
if (filter && packageInfo.packageManager !== filter) continue;
|
|
1961
|
+
packages.push(packageInfo);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return packages;
|
|
1927
1965
|
}
|
|
1928
1966
|
/**
|
|
1929
|
-
*
|
|
1930
|
-
* @
|
|
1931
|
-
* @
|
|
1967
|
+
* Prefetch all packages from the configuration
|
|
1968
|
+
* @returns Summary of prefetch results
|
|
1969
|
+
* @throws Error if prefetch operation fails unexpectedly
|
|
1932
1970
|
*/
|
|
1933
|
-
async
|
|
1934
|
-
const filePath = this.getRecordPath(serverId);
|
|
1971
|
+
async prefetch() {
|
|
1935
1972
|
try {
|
|
1936
|
-
const
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1973
|
+
const packages = this.extractPackages();
|
|
1974
|
+
const results = [];
|
|
1975
|
+
if (packages.length === 0) return {
|
|
1976
|
+
totalPackages: 0,
|
|
1977
|
+
successful: 0,
|
|
1978
|
+
failed: 0,
|
|
1979
|
+
results: []
|
|
1980
|
+
};
|
|
1981
|
+
if (this.config.parallel) {
|
|
1982
|
+
const promises = packages.map(async (pkg) => this.prefetchPackage(pkg));
|
|
1983
|
+
results.push(...await Promise.all(promises));
|
|
1984
|
+
} else for (const pkg of packages) {
|
|
1985
|
+
const result = await this.prefetchPackage(pkg);
|
|
1986
|
+
results.push(result);
|
|
1987
|
+
}
|
|
1988
|
+
const successful = results.filter((r) => r.success).length;
|
|
1989
|
+
const failed = results.filter((r) => !r.success).length;
|
|
1990
|
+
return {
|
|
1991
|
+
totalPackages: packages.length,
|
|
1992
|
+
successful,
|
|
1993
|
+
failed,
|
|
1994
|
+
results
|
|
1995
|
+
};
|
|
1939
1996
|
} catch (error) {
|
|
1940
|
-
|
|
1941
|
-
throw new Error(`Failed to read runtime state for server '${serverId}' from '${filePath}': ${toErrorMessage$2(error)}`);
|
|
1997
|
+
throw new Error(`Failed to prefetch packages: ${error instanceof Error ? error.message : String(error)}`);
|
|
1942
1998
|
}
|
|
1943
1999
|
}
|
|
1944
2000
|
/**
|
|
1945
|
-
*
|
|
1946
|
-
* @
|
|
2001
|
+
* Prefetch a single package
|
|
2002
|
+
* @param pkg - Package info to prefetch
|
|
2003
|
+
* @returns Result of the prefetch operation
|
|
1947
2004
|
*/
|
|
1948
|
-
async
|
|
2005
|
+
async prefetchPackage(pkg) {
|
|
1949
2006
|
try {
|
|
1950
|
-
const
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
this.logger.debug(`Skipping unreadable runtime state file ${file.name}`, error);
|
|
1958
|
-
return null;
|
|
1959
|
-
}
|
|
1960
|
-
}))).filter((record) => record !== null);
|
|
2007
|
+
const [command, ...args] = pkg.fullCommand;
|
|
2008
|
+
const result = await this.runCommand(command, args);
|
|
2009
|
+
return {
|
|
2010
|
+
package: pkg,
|
|
2011
|
+
success: result.success,
|
|
2012
|
+
output: result.output
|
|
2013
|
+
};
|
|
1961
2014
|
} catch (error) {
|
|
1962
|
-
|
|
1963
|
-
|
|
2015
|
+
return {
|
|
2016
|
+
package: pkg,
|
|
2017
|
+
success: false,
|
|
2018
|
+
output: error instanceof Error ? error.message : String(error)
|
|
2019
|
+
};
|
|
1964
2020
|
}
|
|
1965
2021
|
}
|
|
1966
2022
|
/**
|
|
1967
|
-
*
|
|
1968
|
-
* @param
|
|
1969
|
-
* @returns
|
|
2023
|
+
* Validate package name to prevent command injection
|
|
2024
|
+
* @param packageName - Package name to validate
|
|
2025
|
+
* @returns True if package name is safe, false otherwise
|
|
2026
|
+
* @remarks Rejects package names containing shell metacharacters
|
|
2027
|
+
* @example
|
|
2028
|
+
* isValidPackageName('@scope/package') // true
|
|
2029
|
+
* isValidPackageName('my-package@1.0.0') // true
|
|
2030
|
+
* isValidPackageName('pkg; rm -rf /') // false (shell injection)
|
|
2031
|
+
* isValidPackageName('pkg$(whoami)') // false (command substitution)
|
|
1970
2032
|
*/
|
|
1971
|
-
|
|
1972
|
-
|
|
2033
|
+
isValidPackageName(packageName) {
|
|
2034
|
+
return VALID_PACKAGE_NAME_PATTERN.test(packageName);
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Extract package info from a server's stdio config
|
|
2038
|
+
* @param serverName - Name of the MCP server
|
|
2039
|
+
* @param config - Stdio configuration for the server
|
|
2040
|
+
* @returns Package info if extractable, null otherwise
|
|
2041
|
+
*/
|
|
2042
|
+
extractPackageInfo(serverName, config) {
|
|
2043
|
+
const command = config.command.toLowerCase();
|
|
2044
|
+
const args = config.args || [];
|
|
2045
|
+
if (command === "npx" || command.endsWith("/npx")) {
|
|
2046
|
+
const packageName = this.extractNpxPackage(args);
|
|
2047
|
+
if (packageName && this.isValidPackageName(packageName)) return {
|
|
2048
|
+
serverName,
|
|
2049
|
+
packageManager: "npx",
|
|
2050
|
+
packageName,
|
|
2051
|
+
fullCommand: [
|
|
2052
|
+
"npm",
|
|
2053
|
+
ARG_INSTALL,
|
|
2054
|
+
"-g",
|
|
2055
|
+
packageName
|
|
2056
|
+
]
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
if (command === "pnpx" || command.endsWith("/pnpx")) {
|
|
2060
|
+
const packageName = this.extractNpxPackage(args);
|
|
2061
|
+
if (packageName && this.isValidPackageName(packageName)) return {
|
|
2062
|
+
serverName,
|
|
2063
|
+
packageManager: COMMAND_PNPX,
|
|
2064
|
+
packageName,
|
|
2065
|
+
fullCommand: [
|
|
2066
|
+
COMMAND_PNPM,
|
|
2067
|
+
"add",
|
|
2068
|
+
"-g",
|
|
2069
|
+
packageName
|
|
2070
|
+
]
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
if (command === "uvx" || command.endsWith("/uvx")) {
|
|
2074
|
+
const packageName = this.extractUvxPackage(args);
|
|
2075
|
+
if (packageName && this.isValidPackageName(packageName)) return {
|
|
2076
|
+
serverName,
|
|
2077
|
+
packageManager: "uvx",
|
|
2078
|
+
packageName,
|
|
2079
|
+
fullCommand: ["uvx", packageName]
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
if ((command === "uv" || command.endsWith("/uv")) && args.includes("run")) {
|
|
2083
|
+
const packageName = this.extractUvRunPackage(args);
|
|
2084
|
+
if (packageName && this.isValidPackageName(packageName)) return {
|
|
2085
|
+
serverName,
|
|
2086
|
+
packageManager: "uv",
|
|
2087
|
+
packageName,
|
|
2088
|
+
fullCommand: [
|
|
2089
|
+
"uv",
|
|
2090
|
+
ARG_TOOL,
|
|
2091
|
+
ARG_INSTALL,
|
|
2092
|
+
packageName
|
|
2093
|
+
]
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Extract package name from npx command args
|
|
2100
|
+
* @param args - Command arguments
|
|
2101
|
+
* @returns Package name or null
|
|
2102
|
+
* @remarks Handles --package=value, --package value, -p value patterns.
|
|
2103
|
+
* Falls back to first non-flag argument if no --package/-p flag found.
|
|
2104
|
+
* Returns null if flag has no value or is followed by another flag.
|
|
2105
|
+
* When multiple --package flags exist, returns the first valid one.
|
|
2106
|
+
* @example
|
|
2107
|
+
* extractNpxPackage(['--package=@scope/pkg']) // returns '@scope/pkg'
|
|
2108
|
+
* extractNpxPackage(['--package', 'pkg-name']) // returns 'pkg-name'
|
|
2109
|
+
* extractNpxPackage(['-p', 'pkg']) // returns 'pkg'
|
|
2110
|
+
* extractNpxPackage(['-y', 'pkg-name', '--flag']) // returns 'pkg-name' (fallback)
|
|
2111
|
+
* extractNpxPackage(['--package=']) // returns null (empty value)
|
|
2112
|
+
*/
|
|
2113
|
+
extractNpxPackage(args) {
|
|
2114
|
+
for (let i = 0; i < args.length; i++) {
|
|
2115
|
+
const arg = args[i];
|
|
2116
|
+
if (arg.startsWith("--package=")) return arg.slice(10) || null;
|
|
2117
|
+
if (arg === "--package" && i + 1 < args.length) {
|
|
2118
|
+
const nextArg = args[i + 1];
|
|
2119
|
+
if (!nextArg.startsWith("-")) return nextArg;
|
|
2120
|
+
}
|
|
2121
|
+
if (arg === "-p" && i + 1 < args.length) {
|
|
2122
|
+
const nextArg = args[i + 1];
|
|
2123
|
+
if (!nextArg.startsWith("-")) return nextArg;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
for (const arg of args) {
|
|
2127
|
+
if (arg.startsWith("-")) continue;
|
|
2128
|
+
return arg;
|
|
2129
|
+
}
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Extract package name from uvx command args
|
|
2134
|
+
* @param args - Command arguments
|
|
2135
|
+
* @returns Package name or null
|
|
2136
|
+
* @remarks Assumes the first non-flag argument is the package name.
|
|
2137
|
+
* Handles both single (-) and double (--) dash flags.
|
|
2138
|
+
* @example
|
|
2139
|
+
* extractUvxPackage(['mcp-server-fetch']) // returns 'mcp-server-fetch'
|
|
2140
|
+
* extractUvxPackage(['--quiet', 'pkg-name']) // returns 'pkg-name'
|
|
2141
|
+
*/
|
|
2142
|
+
extractUvxPackage(args) {
|
|
2143
|
+
for (const arg of args) {
|
|
2144
|
+
if (arg.startsWith("-")) continue;
|
|
2145
|
+
return arg;
|
|
2146
|
+
}
|
|
2147
|
+
return null;
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Extract package name from uv run command args
|
|
2151
|
+
* @param args - Command arguments
|
|
2152
|
+
* @returns Package name or null
|
|
2153
|
+
* @remarks Looks for the first non-flag argument after the 'run' subcommand.
|
|
2154
|
+
* Returns null if 'run' is not found in args.
|
|
2155
|
+
* @example
|
|
2156
|
+
* extractUvRunPackage(['run', 'mcp-server']) // returns 'mcp-server'
|
|
2157
|
+
* extractUvRunPackage(['run', '--verbose', 'pkg']) // returns 'pkg'
|
|
2158
|
+
* extractUvRunPackage(['install', 'pkg']) // returns null (no 'run')
|
|
2159
|
+
*/
|
|
2160
|
+
extractUvRunPackage(args) {
|
|
2161
|
+
const runIndex = args.indexOf("run");
|
|
2162
|
+
if (runIndex === -1) return null;
|
|
2163
|
+
for (let i = runIndex + 1; i < args.length; i++) {
|
|
2164
|
+
const arg = args[i];
|
|
2165
|
+
if (arg.startsWith("-")) continue;
|
|
2166
|
+
return arg;
|
|
2167
|
+
}
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Run a shell command and capture output
|
|
2172
|
+
* @param command - Command to run
|
|
2173
|
+
* @param args - Command arguments
|
|
2174
|
+
* @returns Promise with success status and output
|
|
2175
|
+
*/
|
|
2176
|
+
runCommand(command, args) {
|
|
2177
|
+
return new Promise((resolve) => {
|
|
2178
|
+
const proc = spawn(command, args, {
|
|
2179
|
+
stdio: [
|
|
2180
|
+
STDIO_IGNORE,
|
|
2181
|
+
STDIO_PIPE,
|
|
2182
|
+
STDIO_PIPE
|
|
2183
|
+
],
|
|
2184
|
+
shell: process.platform === PLATFORM_WIN32
|
|
2185
|
+
});
|
|
2186
|
+
let stdout = "";
|
|
2187
|
+
let stderr = "";
|
|
2188
|
+
proc.stdout?.on("data", (data) => {
|
|
2189
|
+
stdout += data.toString();
|
|
2190
|
+
});
|
|
2191
|
+
proc.stderr?.on("data", (data) => {
|
|
2192
|
+
stderr += data.toString();
|
|
2193
|
+
});
|
|
2194
|
+
proc.on("close", (code) => {
|
|
2195
|
+
resolve({
|
|
2196
|
+
success: code === 0,
|
|
2197
|
+
output: stdout || stderr
|
|
2198
|
+
});
|
|
2199
|
+
});
|
|
2200
|
+
proc.on("error", (error) => {
|
|
2201
|
+
resolve({
|
|
2202
|
+
success: false,
|
|
2203
|
+
output: error.message
|
|
2204
|
+
});
|
|
2205
|
+
});
|
|
2206
|
+
});
|
|
1973
2207
|
}
|
|
1974
2208
|
};
|
|
1975
|
-
/** Path for the runtime health check endpoint. */
|
|
1976
|
-
const HEALTH_CHECK_PATH = "/health";
|
|
1977
|
-
/** Path for the authenticated admin shutdown endpoint. */
|
|
1978
|
-
const ADMIN_SHUTDOWN_PATH = "/admin/shutdown";
|
|
1979
|
-
/** HTTP POST method identifier. */
|
|
1980
|
-
const HTTP_METHOD_POST = "POST";
|
|
1981
|
-
/** HTTP header name for bearer token authorization. */
|
|
1982
|
-
const AUTHORIZATION_HEADER_NAME = "Authorization";
|
|
1983
|
-
/** Prefix for bearer token values in the Authorization header. */
|
|
1984
|
-
const BEARER_TOKEN_PREFIX = "Bearer ";
|
|
1985
|
-
/** HTTP protocol scheme prefix for URL construction. */
|
|
1986
|
-
const HTTP_PROTOCOL = "http://";
|
|
1987
|
-
/** Hosts that are safe to send admin requests to (loopback only). */
|
|
1988
|
-
const ALLOWED_HOSTS = new Set([
|
|
1989
|
-
"localhost",
|
|
1990
|
-
"127.0.0.1",
|
|
1991
|
-
"::1"
|
|
1992
|
-
]);
|
|
1993
2209
|
//#endregion
|
|
1994
|
-
//#region src/services/
|
|
1995
|
-
/**
|
|
1996
|
-
* Safely cast a non-null object to a string-keyed record for property access.
|
|
1997
|
-
* @param value - Object value already verified as non-null
|
|
1998
|
-
* @returns The same value typed as a record
|
|
1999
|
-
*/
|
|
2000
|
-
function toRecord(value) {
|
|
2001
|
-
return value;
|
|
2002
|
-
}
|
|
2003
|
-
/**
|
|
2004
|
-
* Type guard for health responses.
|
|
2005
|
-
* @param value - Candidate payload to validate
|
|
2006
|
-
* @returns True when payload matches health response shape
|
|
2007
|
-
*/
|
|
2008
|
-
function isHealthResponse(value) {
|
|
2009
|
-
if (typeof value !== "object" || value === null) return false;
|
|
2010
|
-
const record = toRecord(value);
|
|
2011
|
-
return "status" in record && record["status"] === "ok" && "transport" in record && record["transport"] === "http" && (!("serverId" in record) || record["serverId"] === void 0 || typeof record["serverId"] === "string");
|
|
2012
|
-
}
|
|
2210
|
+
//#region src/services/RuntimeStateService.ts
|
|
2013
2211
|
/**
|
|
2014
|
-
*
|
|
2015
|
-
*
|
|
2016
|
-
*
|
|
2212
|
+
* RuntimeStateService
|
|
2213
|
+
*
|
|
2214
|
+
* Persists runtime metadata for HTTP mcp-proxy instances so external commands
|
|
2215
|
+
* (for example `mcp-proxy stop`) can discover and target the correct server.
|
|
2017
2216
|
*/
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
return
|
|
2217
|
+
const RUNTIME_DIR_NAME = "runtimes";
|
|
2218
|
+
const RUNTIME_FILE_SUFFIX = ".runtime.json";
|
|
2219
|
+
function isObject(value) {
|
|
2220
|
+
return typeof value === "object" && value !== null;
|
|
2022
2221
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
* Format runtime endpoint URL after validating the host is a loopback address.
|
|
2027
|
-
* Rejects non-loopback hosts to prevent SSRF via tampered runtime state files.
|
|
2028
|
-
* @param runtime - Runtime record to format
|
|
2029
|
-
* @param path - Request path to append
|
|
2030
|
-
* @returns Full runtime URL
|
|
2031
|
-
*/
|
|
2032
|
-
function buildRuntimeUrl(runtime, path) {
|
|
2033
|
-
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.`);
|
|
2034
|
-
return `${HTTP_PROTOCOL}${runtime.host}:${runtime.port}${path}`;
|
|
2222
|
+
function isRuntimeStateRecord(value) {
|
|
2223
|
+
if (!isObject(value)) return false;
|
|
2224
|
+
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");
|
|
2035
2225
|
}
|
|
2036
|
-
function toErrorMessage$
|
|
2226
|
+
function toErrorMessage$2(error) {
|
|
2037
2227
|
return error instanceof Error ? error.message : String(error);
|
|
2038
2228
|
}
|
|
2039
|
-
function sleep(delayMs) {
|
|
2040
|
-
return new Promise((resolve) => {
|
|
2041
|
-
setTimeout(resolve, delayMs);
|
|
2042
|
-
});
|
|
2043
|
-
}
|
|
2044
2229
|
/**
|
|
2045
|
-
*
|
|
2230
|
+
* Runtime state persistence implementation.
|
|
2046
2231
|
*/
|
|
2047
|
-
var
|
|
2048
|
-
|
|
2232
|
+
var RuntimeStateService = class RuntimeStateService {
|
|
2233
|
+
runtimeDir;
|
|
2049
2234
|
logger;
|
|
2050
|
-
constructor(
|
|
2051
|
-
this.
|
|
2235
|
+
constructor(runtimeDir, logger = console) {
|
|
2236
|
+
this.runtimeDir = runtimeDir ?? RuntimeStateService.getDefaultRuntimeDir();
|
|
2052
2237
|
this.logger = logger;
|
|
2053
2238
|
}
|
|
2054
2239
|
/**
|
|
2055
|
-
* Resolve
|
|
2056
|
-
* @
|
|
2057
|
-
* @returns Stop result payload
|
|
2240
|
+
* Resolve default runtime directory under the user's home cache path.
|
|
2241
|
+
* @returns Absolute runtime directory path
|
|
2058
2242
|
*/
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
const runtime = await this.resolveRuntime(request);
|
|
2062
|
-
const health = await this.fetchHealth(runtime, timeoutMs);
|
|
2063
|
-
if (!health.reachable) {
|
|
2064
|
-
await this.runtimeStateService.remove(runtime.serverId);
|
|
2065
|
-
throw new Error(`Runtime '${runtime.serverId}' is not reachable at http://${runtime.host}:${runtime.port}. Removed stale runtime record.`);
|
|
2066
|
-
}
|
|
2067
|
-
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.`);
|
|
2068
|
-
const shutdownToken = request.token ?? runtime.shutdownToken;
|
|
2069
|
-
if (!shutdownToken) throw new Error(`No shutdown token available for runtime '${runtime.serverId}'.`);
|
|
2070
|
-
const shutdownResponse = await this.requestShutdown(runtime, shutdownToken, timeoutMs);
|
|
2071
|
-
await this.waitForShutdown(runtime, timeoutMs);
|
|
2072
|
-
await this.runtimeStateService.remove(runtime.serverId);
|
|
2073
|
-
return {
|
|
2074
|
-
ok: true,
|
|
2075
|
-
serverId: runtime.serverId,
|
|
2076
|
-
host: runtime.host,
|
|
2077
|
-
port: runtime.port,
|
|
2078
|
-
message: shutdownResponse.message
|
|
2079
|
-
};
|
|
2243
|
+
static getDefaultRuntimeDir() {
|
|
2244
|
+
return join(homedir(), ".aicode-toolkit", "mcp-proxy", RUNTIME_DIR_NAME);
|
|
2080
2245
|
}
|
|
2081
2246
|
/**
|
|
2082
|
-
*
|
|
2083
|
-
* @param
|
|
2084
|
-
* @returns
|
|
2247
|
+
* Build runtime state file path for a given server ID.
|
|
2248
|
+
* @param serverId - Target mcp-proxy server identifier
|
|
2249
|
+
* @returns Absolute runtime file path
|
|
2085
2250
|
*/
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
const runtime = await this.runtimeStateService.read(request.serverId);
|
|
2089
|
-
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.`);
|
|
2090
|
-
return runtime;
|
|
2091
|
-
}
|
|
2092
|
-
if (request.host === void 0 || request.port === void 0) throw new Error("Provide --id or both --host and --port to select a runtime.");
|
|
2093
|
-
const matches = (await this.runtimeStateService.list()).filter((runtime) => runtime.host === request.host && runtime.port === request.port);
|
|
2094
|
-
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.`);
|
|
2095
|
-
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.`);
|
|
2096
|
-
return matches[0];
|
|
2251
|
+
getRecordPath(serverId) {
|
|
2252
|
+
return join(this.runtimeDir, `${serverId}${RUNTIME_FILE_SUFFIX}`);
|
|
2097
2253
|
}
|
|
2098
2254
|
/**
|
|
2099
|
-
*
|
|
2100
|
-
* @param
|
|
2101
|
-
* @
|
|
2102
|
-
* @returns Reachability status and optional payload
|
|
2255
|
+
* Persist a runtime state record.
|
|
2256
|
+
* @param record - Runtime metadata to persist
|
|
2257
|
+
* @returns Promise that resolves when write completes
|
|
2103
2258
|
*/
|
|
2104
|
-
async
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
if (!response.ok) return { reachable: false };
|
|
2108
|
-
const payload = await response.json();
|
|
2109
|
-
if (!isHealthResponse(payload)) throw new Error("Received invalid health response payload.");
|
|
2110
|
-
return {
|
|
2111
|
-
reachable: true,
|
|
2112
|
-
payload
|
|
2113
|
-
};
|
|
2114
|
-
} catch (error) {
|
|
2115
|
-
this.logger.debug(`Health check failed for ${runtime.serverId}`, error);
|
|
2116
|
-
return { reachable: false };
|
|
2117
|
-
}
|
|
2259
|
+
async write(record) {
|
|
2260
|
+
await mkdir(this.runtimeDir, { recursive: true });
|
|
2261
|
+
await writeFile(this.getRecordPath(record.serverId), JSON.stringify(record, null, 2), "utf-8");
|
|
2118
2262
|
}
|
|
2119
2263
|
/**
|
|
2120
|
-
*
|
|
2121
|
-
* @param
|
|
2122
|
-
* @
|
|
2123
|
-
* @param timeoutMs - Request timeout in milliseconds
|
|
2124
|
-
* @returns Parsed shutdown response payload
|
|
2264
|
+
* Read a runtime state record by server ID.
|
|
2265
|
+
* @param serverId - Target mcp-proxy server identifier
|
|
2266
|
+
* @returns Matching runtime record, or null when no record exists
|
|
2125
2267
|
*/
|
|
2126
|
-
async
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2268
|
+
async read(serverId) {
|
|
2269
|
+
const filePath = this.getRecordPath(serverId);
|
|
2270
|
+
try {
|
|
2271
|
+
const content = await readFile(filePath, "utf-8");
|
|
2272
|
+
const parsed = JSON.parse(content);
|
|
2273
|
+
return isRuntimeStateRecord(parsed) ? parsed : null;
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
if (isObject(error) && "code" in error && error.code === "ENOENT") return null;
|
|
2276
|
+
throw new Error(`Failed to read runtime state for server '${serverId}' from '${filePath}': ${toErrorMessage$2(error)}`);
|
|
2277
|
+
}
|
|
2135
2278
|
}
|
|
2136
2279
|
/**
|
|
2137
|
-
*
|
|
2138
|
-
* @
|
|
2139
|
-
* @param timeoutMs - Maximum wait time in milliseconds
|
|
2140
|
-
* @returns Promise that resolves when shutdown is observed
|
|
2280
|
+
* List all persisted runtime records.
|
|
2281
|
+
* @returns Array of runtime records
|
|
2141
2282
|
*/
|
|
2142
|
-
async
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2283
|
+
async list() {
|
|
2284
|
+
try {
|
|
2285
|
+
const files = (await readdir(this.runtimeDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(RUNTIME_FILE_SUFFIX));
|
|
2286
|
+
return (await Promise.all(files.map(async (file) => {
|
|
2287
|
+
try {
|
|
2288
|
+
const content = await readFile(join(this.runtimeDir, file.name), "utf-8");
|
|
2289
|
+
const parsed = JSON.parse(content);
|
|
2290
|
+
return isRuntimeStateRecord(parsed) ? parsed : null;
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
this.logger.debug(`Skipping unreadable runtime state file ${file.name}`, error);
|
|
2293
|
+
return null;
|
|
2294
|
+
}
|
|
2295
|
+
}))).filter((record) => record !== null);
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
if (isObject(error) && "code" in error && error.code === "ENOENT") return [];
|
|
2298
|
+
throw new Error(`Failed to list runtime states from '${this.runtimeDir}': ${toErrorMessage$2(error)}`);
|
|
2147
2299
|
}
|
|
2148
|
-
throw new Error(`Timed out waiting for runtime '${runtime.serverId}' to stop at http://${runtime.host}:${runtime.port}.`);
|
|
2149
2300
|
}
|
|
2150
2301
|
/**
|
|
2151
|
-
*
|
|
2152
|
-
* @param
|
|
2153
|
-
* @
|
|
2154
|
-
* @param timeoutMs - Timeout in milliseconds
|
|
2155
|
-
* @returns Fetch response
|
|
2302
|
+
* Remove a runtime state record by server ID.
|
|
2303
|
+
* @param serverId - Target mcp-proxy server identifier
|
|
2304
|
+
* @returns Promise that resolves when delete completes
|
|
2156
2305
|
*/
|
|
2157
|
-
async
|
|
2158
|
-
|
|
2159
|
-
const timeoutId = setTimeout(() => {
|
|
2160
|
-
controller.abort();
|
|
2161
|
-
}, timeoutMs);
|
|
2162
|
-
try {
|
|
2163
|
-
return await fetch(url, {
|
|
2164
|
-
...init,
|
|
2165
|
-
signal: controller.signal
|
|
2166
|
-
});
|
|
2167
|
-
} catch (error) {
|
|
2168
|
-
throw new Error(`Request to '${url}' failed: ${toErrorMessage$1(error)}`);
|
|
2169
|
-
} finally {
|
|
2170
|
-
clearTimeout(timeoutId);
|
|
2171
|
-
}
|
|
2306
|
+
async remove(serverId) {
|
|
2307
|
+
await rm(this.getRecordPath(serverId), { force: true });
|
|
2172
2308
|
}
|
|
2173
2309
|
};
|
|
2174
2310
|
//#endregion
|
|
@@ -2527,339 +2663,203 @@ var SkillService = class {
|
|
|
2527
2663
|
};
|
|
2528
2664
|
}
|
|
2529
2665
|
};
|
|
2530
|
-
/**
|
|
2531
|
-
const
|
|
2532
|
-
/**
|
|
2533
|
-
const
|
|
2534
|
-
/**
|
|
2535
|
-
const
|
|
2536
|
-
/**
|
|
2537
|
-
const
|
|
2666
|
+
/** Path for the runtime health check endpoint. */
|
|
2667
|
+
const HEALTH_CHECK_PATH = "/health";
|
|
2668
|
+
/** Path for the authenticated admin shutdown endpoint. */
|
|
2669
|
+
const ADMIN_SHUTDOWN_PATH = "/admin/shutdown";
|
|
2670
|
+
/** HTTP POST method identifier. */
|
|
2671
|
+
const HTTP_METHOD_POST = "POST";
|
|
2672
|
+
/** HTTP header name for bearer token authorization. */
|
|
2673
|
+
const AUTHORIZATION_HEADER_NAME = "Authorization";
|
|
2674
|
+
/** Prefix for bearer token values in the Authorization header. */
|
|
2675
|
+
const BEARER_TOKEN_PREFIX = "Bearer ";
|
|
2676
|
+
/** HTTP protocol scheme prefix for URL construction. */
|
|
2677
|
+
const HTTP_PROTOCOL = "http://";
|
|
2678
|
+
/** Hosts that are safe to send admin requests to (loopback only). */
|
|
2679
|
+
const ALLOWED_HOSTS = new Set([
|
|
2680
|
+
"localhost",
|
|
2681
|
+
"127.0.0.1",
|
|
2682
|
+
"::1"
|
|
2683
|
+
]);
|
|
2684
|
+
//#endregion
|
|
2685
|
+
//#region src/services/StopServerService/types.ts
|
|
2538
2686
|
/**
|
|
2539
|
-
*
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2542
|
-
* @example
|
|
2543
|
-
* // Valid: '@scope/package@1.0.0', 'my-package', 'my_package', '@org/pkg'
|
|
2544
|
-
* // Invalid: 'pkg; rm -rf /', 'pkg$(cmd)', 'pkg`whoami`', 'pkg|cat /etc/passwd'
|
|
2687
|
+
* Safely cast a non-null object to a string-keyed record for property access.
|
|
2688
|
+
* @param value - Object value already verified as non-null
|
|
2689
|
+
* @returns The same value typed as a record
|
|
2545
2690
|
*/
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
/** Stdio option to ignore stream */
|
|
2550
|
-
const STDIO_IGNORE = "ignore";
|
|
2551
|
-
/** Stdio option to pipe stream */
|
|
2552
|
-
const STDIO_PIPE = "pipe";
|
|
2553
|
-
//#endregion
|
|
2554
|
-
//#region src/services/PrefetchService/PrefetchService.ts
|
|
2691
|
+
function toRecord(value) {
|
|
2692
|
+
return value;
|
|
2693
|
+
}
|
|
2555
2694
|
/**
|
|
2556
|
-
*
|
|
2557
|
-
*
|
|
2558
|
-
*
|
|
2559
|
-
* - Service pattern for business logic encapsulation
|
|
2560
|
-
* - Single responsibility principle
|
|
2561
|
-
*
|
|
2562
|
-
* CODING STANDARDS:
|
|
2563
|
-
* - Use async/await for asynchronous operations
|
|
2564
|
-
* - Throw descriptive errors for error cases
|
|
2565
|
-
* - Keep methods focused and well-named
|
|
2566
|
-
* - Document complex logic with comments
|
|
2567
|
-
*
|
|
2568
|
-
* AVOID:
|
|
2569
|
-
* - Mixing concerns (keep focused on single domain)
|
|
2570
|
-
* - Direct tool implementation (services should be tool-agnostic)
|
|
2695
|
+
* Type guard for health responses.
|
|
2696
|
+
* @param value - Candidate payload to validate
|
|
2697
|
+
* @returns True when payload matches health response shape
|
|
2571
2698
|
*/
|
|
2699
|
+
function isHealthResponse(value) {
|
|
2700
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2701
|
+
const record = toRecord(value);
|
|
2702
|
+
return "status" in record && record["status"] === "ok" && "transport" in record && record["transport"] === "http" && (!("serverId" in record) || record["serverId"] === void 0 || typeof record["serverId"] === "string");
|
|
2703
|
+
}
|
|
2572
2704
|
/**
|
|
2573
|
-
* Type guard
|
|
2574
|
-
* @param
|
|
2575
|
-
* @returns True
|
|
2705
|
+
* Type guard for shutdown responses.
|
|
2706
|
+
* @param value - Candidate payload to validate
|
|
2707
|
+
* @returns True when payload matches shutdown response shape
|
|
2576
2708
|
*/
|
|
2577
|
-
function
|
|
2578
|
-
|
|
2709
|
+
function isShutdownResponse(value) {
|
|
2710
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2711
|
+
const record = toRecord(value);
|
|
2712
|
+
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");
|
|
2579
2713
|
}
|
|
2714
|
+
//#endregion
|
|
2715
|
+
//#region src/services/StopServerService/StopServerService.ts
|
|
2580
2716
|
/**
|
|
2581
|
-
*
|
|
2582
|
-
*
|
|
2583
|
-
*
|
|
2584
|
-
* @
|
|
2585
|
-
*
|
|
2586
|
-
* const service = new PrefetchService({
|
|
2587
|
-
* mcpConfig: await configFetcher.fetchConfiguration(),
|
|
2588
|
-
* parallel: true,
|
|
2589
|
-
* });
|
|
2590
|
-
* const packages = service.extractPackages();
|
|
2591
|
-
* const summary = await service.prefetch();
|
|
2592
|
-
* ```
|
|
2717
|
+
* Format runtime endpoint URL after validating the host is a loopback address.
|
|
2718
|
+
* Rejects non-loopback hosts to prevent SSRF via tampered runtime state files.
|
|
2719
|
+
* @param runtime - Runtime record to format
|
|
2720
|
+
* @param path - Request path to append
|
|
2721
|
+
* @returns Full runtime URL
|
|
2593
2722
|
*/
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
if (packageInfo) {
|
|
2616
|
-
if (filter && packageInfo.packageManager !== filter) continue;
|
|
2617
|
-
packages.push(packageInfo);
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
return packages;
|
|
2621
|
-
}
|
|
2622
|
-
/**
|
|
2623
|
-
* Prefetch all packages from the configuration
|
|
2624
|
-
* @returns Summary of prefetch results
|
|
2625
|
-
* @throws Error if prefetch operation fails unexpectedly
|
|
2626
|
-
*/
|
|
2627
|
-
async prefetch() {
|
|
2628
|
-
try {
|
|
2629
|
-
const packages = this.extractPackages();
|
|
2630
|
-
const results = [];
|
|
2631
|
-
if (packages.length === 0) return {
|
|
2632
|
-
totalPackages: 0,
|
|
2633
|
-
successful: 0,
|
|
2634
|
-
failed: 0,
|
|
2635
|
-
results: []
|
|
2636
|
-
};
|
|
2637
|
-
if (this.config.parallel) {
|
|
2638
|
-
const promises = packages.map(async (pkg) => this.prefetchPackage(pkg));
|
|
2639
|
-
results.push(...await Promise.all(promises));
|
|
2640
|
-
} else for (const pkg of packages) {
|
|
2641
|
-
const result = await this.prefetchPackage(pkg);
|
|
2642
|
-
results.push(result);
|
|
2643
|
-
}
|
|
2644
|
-
const successful = results.filter((r) => r.success).length;
|
|
2645
|
-
const failed = results.filter((r) => !r.success).length;
|
|
2646
|
-
return {
|
|
2647
|
-
totalPackages: packages.length,
|
|
2648
|
-
successful,
|
|
2649
|
-
failed,
|
|
2650
|
-
results
|
|
2651
|
-
};
|
|
2652
|
-
} catch (error) {
|
|
2653
|
-
throw new Error(`Failed to prefetch packages: ${error instanceof Error ? error.message : String(error)}`);
|
|
2654
|
-
}
|
|
2723
|
+
function buildRuntimeUrl(runtime, path) {
|
|
2724
|
+
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.`);
|
|
2725
|
+
return `${HTTP_PROTOCOL}${runtime.host}:${runtime.port}${path}`;
|
|
2726
|
+
}
|
|
2727
|
+
function toErrorMessage$1(error) {
|
|
2728
|
+
return error instanceof Error ? error.message : String(error);
|
|
2729
|
+
}
|
|
2730
|
+
function sleep(delayMs) {
|
|
2731
|
+
return new Promise((resolve) => {
|
|
2732
|
+
setTimeout(resolve, delayMs);
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Service for resolving runtime targets and stopping them safely.
|
|
2737
|
+
*/
|
|
2738
|
+
var StopServerService = class {
|
|
2739
|
+
runtimeStateService;
|
|
2740
|
+
logger;
|
|
2741
|
+
constructor(runtimeStateService = new RuntimeStateService(), logger = console) {
|
|
2742
|
+
this.runtimeStateService = runtimeStateService;
|
|
2743
|
+
this.logger = logger;
|
|
2655
2744
|
}
|
|
2656
2745
|
/**
|
|
2657
|
-
*
|
|
2658
|
-
* @param
|
|
2659
|
-
* @returns
|
|
2746
|
+
* Resolve a target runtime and stop it cooperatively.
|
|
2747
|
+
* @param request - Stop request options
|
|
2748
|
+
* @returns Stop result payload
|
|
2660
2749
|
*/
|
|
2661
|
-
async
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
output: result.output
|
|
2669
|
-
};
|
|
2670
|
-
} catch (error) {
|
|
2671
|
-
return {
|
|
2672
|
-
package: pkg,
|
|
2673
|
-
success: false,
|
|
2674
|
-
output: error instanceof Error ? error.message : String(error)
|
|
2675
|
-
};
|
|
2750
|
+
async stop(request) {
|
|
2751
|
+
const timeoutMs = request.timeoutMs ?? 5e3;
|
|
2752
|
+
const runtime = await this.resolveRuntime(request);
|
|
2753
|
+
const health = await this.fetchHealth(runtime, timeoutMs);
|
|
2754
|
+
if (!health.reachable) {
|
|
2755
|
+
await this.runtimeStateService.remove(runtime.serverId);
|
|
2756
|
+
throw new Error(`Runtime '${runtime.serverId}' is not reachable at http://${runtime.host}:${runtime.port}. Removed stale runtime record.`);
|
|
2676
2757
|
}
|
|
2758
|
+
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.`);
|
|
2759
|
+
const shutdownToken = request.token ?? runtime.shutdownToken;
|
|
2760
|
+
if (!shutdownToken) throw new Error(`No shutdown token available for runtime '${runtime.serverId}'.`);
|
|
2761
|
+
const shutdownResponse = await this.requestShutdown(runtime, shutdownToken, timeoutMs);
|
|
2762
|
+
await this.waitForShutdown(runtime, timeoutMs);
|
|
2763
|
+
await this.runtimeStateService.remove(runtime.serverId);
|
|
2764
|
+
return {
|
|
2765
|
+
ok: true,
|
|
2766
|
+
serverId: runtime.serverId,
|
|
2767
|
+
host: runtime.host,
|
|
2768
|
+
port: runtime.port,
|
|
2769
|
+
message: shutdownResponse.message
|
|
2770
|
+
};
|
|
2677
2771
|
}
|
|
2678
2772
|
/**
|
|
2679
|
-
*
|
|
2680
|
-
* @param
|
|
2681
|
-
* @returns
|
|
2682
|
-
* @remarks Rejects package names containing shell metacharacters
|
|
2683
|
-
* @example
|
|
2684
|
-
* isValidPackageName('@scope/package') // true
|
|
2685
|
-
* isValidPackageName('my-package@1.0.0') // true
|
|
2686
|
-
* isValidPackageName('pkg; rm -rf /') // false (shell injection)
|
|
2687
|
-
* isValidPackageName('pkg$(whoami)') // false (command substitution)
|
|
2688
|
-
*/
|
|
2689
|
-
isValidPackageName(packageName) {
|
|
2690
|
-
return VALID_PACKAGE_NAME_PATTERN.test(packageName);
|
|
2691
|
-
}
|
|
2692
|
-
/**
|
|
2693
|
-
* Extract package info from a server's stdio config
|
|
2694
|
-
* @param serverName - Name of the MCP server
|
|
2695
|
-
* @param config - Stdio configuration for the server
|
|
2696
|
-
* @returns Package info if extractable, null otherwise
|
|
2773
|
+
* Resolve a runtime record from explicit ID or a unique host/port pair.
|
|
2774
|
+
* @param request - Stop request options
|
|
2775
|
+
* @returns Matching runtime record
|
|
2697
2776
|
*/
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
if (packageName && this.isValidPackageName(packageName)) return {
|
|
2704
|
-
serverName,
|
|
2705
|
-
packageManager: "npx",
|
|
2706
|
-
packageName,
|
|
2707
|
-
fullCommand: [
|
|
2708
|
-
"npm",
|
|
2709
|
-
ARG_INSTALL,
|
|
2710
|
-
"-g",
|
|
2711
|
-
packageName
|
|
2712
|
-
]
|
|
2713
|
-
};
|
|
2714
|
-
}
|
|
2715
|
-
if (command === "pnpx" || command.endsWith("/pnpx")) {
|
|
2716
|
-
const packageName = this.extractNpxPackage(args);
|
|
2717
|
-
if (packageName && this.isValidPackageName(packageName)) return {
|
|
2718
|
-
serverName,
|
|
2719
|
-
packageManager: COMMAND_PNPX,
|
|
2720
|
-
packageName,
|
|
2721
|
-
fullCommand: [
|
|
2722
|
-
COMMAND_PNPM,
|
|
2723
|
-
"add",
|
|
2724
|
-
"-g",
|
|
2725
|
-
packageName
|
|
2726
|
-
]
|
|
2727
|
-
};
|
|
2728
|
-
}
|
|
2729
|
-
if (command === "uvx" || command.endsWith("/uvx")) {
|
|
2730
|
-
const packageName = this.extractUvxPackage(args);
|
|
2731
|
-
if (packageName && this.isValidPackageName(packageName)) return {
|
|
2732
|
-
serverName,
|
|
2733
|
-
packageManager: "uvx",
|
|
2734
|
-
packageName,
|
|
2735
|
-
fullCommand: ["uvx", packageName]
|
|
2736
|
-
};
|
|
2737
|
-
}
|
|
2738
|
-
if ((command === "uv" || command.endsWith("/uv")) && args.includes("run")) {
|
|
2739
|
-
const packageName = this.extractUvRunPackage(args);
|
|
2740
|
-
if (packageName && this.isValidPackageName(packageName)) return {
|
|
2741
|
-
serverName,
|
|
2742
|
-
packageManager: "uv",
|
|
2743
|
-
packageName,
|
|
2744
|
-
fullCommand: [
|
|
2745
|
-
"uv",
|
|
2746
|
-
ARG_TOOL,
|
|
2747
|
-
ARG_INSTALL,
|
|
2748
|
-
packageName
|
|
2749
|
-
]
|
|
2750
|
-
};
|
|
2777
|
+
async resolveRuntime(request) {
|
|
2778
|
+
if (request.serverId) {
|
|
2779
|
+
const runtime = await this.runtimeStateService.read(request.serverId);
|
|
2780
|
+
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.`);
|
|
2781
|
+
return runtime;
|
|
2751
2782
|
}
|
|
2752
|
-
|
|
2783
|
+
if (request.host === void 0 || request.port === void 0) throw new Error("Provide --id or both --host and --port to select a runtime.");
|
|
2784
|
+
const matches = (await this.runtimeStateService.list()).filter((runtime) => runtime.host === request.host && runtime.port === request.port);
|
|
2785
|
+
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.`);
|
|
2786
|
+
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.`);
|
|
2787
|
+
return matches[0];
|
|
2753
2788
|
}
|
|
2754
2789
|
/**
|
|
2755
|
-
*
|
|
2756
|
-
* @param
|
|
2757
|
-
* @
|
|
2758
|
-
* @
|
|
2759
|
-
* Falls back to first non-flag argument if no --package/-p flag found.
|
|
2760
|
-
* Returns null if flag has no value or is followed by another flag.
|
|
2761
|
-
* When multiple --package flags exist, returns the first valid one.
|
|
2762
|
-
* @example
|
|
2763
|
-
* extractNpxPackage(['--package=@scope/pkg']) // returns '@scope/pkg'
|
|
2764
|
-
* extractNpxPackage(['--package', 'pkg-name']) // returns 'pkg-name'
|
|
2765
|
-
* extractNpxPackage(['-p', 'pkg']) // returns 'pkg'
|
|
2766
|
-
* extractNpxPackage(['-y', 'pkg-name', '--flag']) // returns 'pkg-name' (fallback)
|
|
2767
|
-
* extractNpxPackage(['--package=']) // returns null (empty value)
|
|
2790
|
+
* Read the runtime health payload.
|
|
2791
|
+
* @param runtime - Runtime to query
|
|
2792
|
+
* @param timeoutMs - Request timeout in milliseconds
|
|
2793
|
+
* @returns Reachability status and optional payload
|
|
2768
2794
|
*/
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
const
|
|
2772
|
-
if (
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
for (const arg of args) {
|
|
2783
|
-
if (arg.startsWith("-")) continue;
|
|
2784
|
-
return arg;
|
|
2795
|
+
async fetchHealth(runtime, timeoutMs) {
|
|
2796
|
+
try {
|
|
2797
|
+
const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, HEALTH_CHECK_PATH), { method: "GET" }, timeoutMs);
|
|
2798
|
+
if (!response.ok) return { reachable: false };
|
|
2799
|
+
const payload = await response.json();
|
|
2800
|
+
if (!isHealthResponse(payload)) throw new Error("Received invalid health response payload.");
|
|
2801
|
+
return {
|
|
2802
|
+
reachable: true,
|
|
2803
|
+
payload
|
|
2804
|
+
};
|
|
2805
|
+
} catch (error) {
|
|
2806
|
+
this.logger.debug(`Health check failed for ${runtime.serverId}`, error);
|
|
2807
|
+
return { reachable: false };
|
|
2785
2808
|
}
|
|
2786
|
-
return null;
|
|
2787
2809
|
}
|
|
2788
2810
|
/**
|
|
2789
|
-
*
|
|
2790
|
-
* @param
|
|
2791
|
-
* @
|
|
2792
|
-
* @
|
|
2793
|
-
*
|
|
2794
|
-
* @example
|
|
2795
|
-
* extractUvxPackage(['mcp-server-fetch']) // returns 'mcp-server-fetch'
|
|
2796
|
-
* extractUvxPackage(['--quiet', 'pkg-name']) // returns 'pkg-name'
|
|
2811
|
+
* Send authenticated shutdown request to the admin endpoint.
|
|
2812
|
+
* @param runtime - Runtime to stop
|
|
2813
|
+
* @param shutdownToken - Bearer token for the admin endpoint
|
|
2814
|
+
* @param timeoutMs - Request timeout in milliseconds
|
|
2815
|
+
* @returns Parsed shutdown response payload
|
|
2797
2816
|
*/
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2817
|
+
async requestShutdown(runtime, shutdownToken, timeoutMs) {
|
|
2818
|
+
const response = await this.fetchWithTimeout(buildRuntimeUrl(runtime, ADMIN_SHUTDOWN_PATH), {
|
|
2819
|
+
method: HTTP_METHOD_POST,
|
|
2820
|
+
headers: { [AUTHORIZATION_HEADER_NAME]: `${BEARER_TOKEN_PREFIX}${shutdownToken}` }
|
|
2821
|
+
}, timeoutMs);
|
|
2822
|
+
const payload = await response.json();
|
|
2823
|
+
if (!isShutdownResponse(payload)) throw new Error("Received invalid shutdown response payload.");
|
|
2824
|
+
if (!response.ok || !payload.ok) throw new Error(payload.message);
|
|
2825
|
+
return payload;
|
|
2804
2826
|
}
|
|
2805
2827
|
/**
|
|
2806
|
-
*
|
|
2807
|
-
* @param
|
|
2808
|
-
* @
|
|
2809
|
-
* @
|
|
2810
|
-
* Returns null if 'run' is not found in args.
|
|
2811
|
-
* @example
|
|
2812
|
-
* extractUvRunPackage(['run', 'mcp-server']) // returns 'mcp-server'
|
|
2813
|
-
* extractUvRunPackage(['run', '--verbose', 'pkg']) // returns 'pkg'
|
|
2814
|
-
* extractUvRunPackage(['install', 'pkg']) // returns null (no 'run')
|
|
2828
|
+
* Poll until the target runtime is no longer reachable.
|
|
2829
|
+
* @param runtime - Runtime expected to stop
|
|
2830
|
+
* @param timeoutMs - Maximum wait time in milliseconds
|
|
2831
|
+
* @returns Promise that resolves when shutdown is observed
|
|
2815
2832
|
*/
|
|
2816
|
-
|
|
2817
|
-
const
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
if (arg.startsWith("-")) continue;
|
|
2822
|
-
return arg;
|
|
2833
|
+
async waitForShutdown(runtime, timeoutMs) {
|
|
2834
|
+
const deadline = Date.now() + timeoutMs;
|
|
2835
|
+
while (Date.now() < deadline) {
|
|
2836
|
+
if (!(await this.fetchHealth(runtime, Math.max(250, deadline - Date.now()))).reachable) return;
|
|
2837
|
+
await sleep(200);
|
|
2823
2838
|
}
|
|
2824
|
-
|
|
2839
|
+
throw new Error(`Timed out waiting for runtime '${runtime.serverId}' to stop at http://${runtime.host}:${runtime.port}.`);
|
|
2825
2840
|
}
|
|
2826
2841
|
/**
|
|
2827
|
-
*
|
|
2828
|
-
* @param
|
|
2829
|
-
* @param
|
|
2830
|
-
* @
|
|
2842
|
+
* Perform a fetch with an abort timeout.
|
|
2843
|
+
* @param url - Target URL
|
|
2844
|
+
* @param init - Fetch options
|
|
2845
|
+
* @param timeoutMs - Timeout in milliseconds
|
|
2846
|
+
* @returns Fetch response
|
|
2831
2847
|
*/
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
});
|
|
2842
|
-
let stdout = "";
|
|
2843
|
-
let stderr = "";
|
|
2844
|
-
proc.stdout?.on("data", (data) => {
|
|
2845
|
-
stdout += data.toString();
|
|
2846
|
-
});
|
|
2847
|
-
proc.stderr?.on("data", (data) => {
|
|
2848
|
-
stderr += data.toString();
|
|
2849
|
-
});
|
|
2850
|
-
proc.on("close", (code) => {
|
|
2851
|
-
resolve({
|
|
2852
|
-
success: code === 0,
|
|
2853
|
-
output: stdout || stderr
|
|
2854
|
-
});
|
|
2855
|
-
});
|
|
2856
|
-
proc.on("error", (error) => {
|
|
2857
|
-
resolve({
|
|
2858
|
-
success: false,
|
|
2859
|
-
output: error.message
|
|
2860
|
-
});
|
|
2848
|
+
async fetchWithTimeout(url, init, timeoutMs) {
|
|
2849
|
+
const controller = new AbortController();
|
|
2850
|
+
const timeoutId = setTimeout(() => {
|
|
2851
|
+
controller.abort();
|
|
2852
|
+
}, timeoutMs);
|
|
2853
|
+
try {
|
|
2854
|
+
return await fetch(url, {
|
|
2855
|
+
...init,
|
|
2856
|
+
signal: controller.signal
|
|
2861
2857
|
});
|
|
2862
|
-
})
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
throw new Error(`Request to '${url}' failed: ${toErrorMessage$1(error)}`);
|
|
2860
|
+
} finally {
|
|
2861
|
+
clearTimeout(timeoutId);
|
|
2862
|
+
}
|
|
2863
2863
|
}
|
|
2864
2864
|
};
|
|
2865
2865
|
//#endregion
|
|
@@ -2906,8 +2906,6 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
2906
2906
|
skillService;
|
|
2907
2907
|
definitionsCacheService;
|
|
2908
2908
|
liquid = new Liquid();
|
|
2909
|
-
/** Cache for auto-detected skills from prompt front-matter */
|
|
2910
|
-
autoDetectedSkillsCache = null;
|
|
2911
2909
|
/** Unique server identifier for this mcp-proxy instance */
|
|
2912
2910
|
serverId;
|
|
2913
2911
|
/**
|
|
@@ -2928,67 +2926,9 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
2928
2926
|
* the skill service cache is invalidated.
|
|
2929
2927
|
*/
|
|
2930
2928
|
clearAutoDetectedSkillsCache() {
|
|
2931
|
-
this.autoDetectedSkillsCache = null;
|
|
2932
2929
|
this.definitionsCacheService.clearLiveCache();
|
|
2933
2930
|
}
|
|
2934
2931
|
/**
|
|
2935
|
-
* Detects and caches skills from prompt front-matter across all connected MCP servers.
|
|
2936
|
-
* Fetches all prompts and checks their content for YAML front-matter with name/description.
|
|
2937
|
-
* Results are cached to avoid repeated fetches.
|
|
2938
|
-
*
|
|
2939
|
-
* Error Handling Strategy:
|
|
2940
|
-
* - Errors are logged to stderr but do not fail the overall detection process
|
|
2941
|
-
* - This ensures partial results are returned even if some servers/prompts fail
|
|
2942
|
-
* - Common failure reasons: server temporarily unavailable, prompt requires arguments,
|
|
2943
|
-
* network timeout, or server doesn't support listPrompts
|
|
2944
|
-
* - Errors are prefixed with [skill-detection] for easy filtering in logs
|
|
2945
|
-
*
|
|
2946
|
-
* @returns Array of auto-detected skills from prompt front-matter
|
|
2947
|
-
*/
|
|
2948
|
-
async detectSkillsFromPromptFrontMatter() {
|
|
2949
|
-
if (this.autoDetectedSkillsCache !== null) return this.autoDetectedSkillsCache;
|
|
2950
|
-
const clients = this.clientManager.getAllClients();
|
|
2951
|
-
let listPromptsFailures = 0;
|
|
2952
|
-
let fetchPromptFailures = 0;
|
|
2953
|
-
const autoDetectedSkills = (await Promise.all(clients.map(async (client) => {
|
|
2954
|
-
const detectedSkills = [];
|
|
2955
|
-
const configuredPromptNames = new Set(client.prompts ? Object.keys(client.prompts) : []);
|
|
2956
|
-
try {
|
|
2957
|
-
const prompts = await client.listPrompts();
|
|
2958
|
-
if (!prompts || prompts.length === 0) return detectedSkills;
|
|
2959
|
-
const promptResults = await Promise.all(prompts.map(async (promptInfo) => {
|
|
2960
|
-
if (configuredPromptNames.has(promptInfo.name)) return null;
|
|
2961
|
-
try {
|
|
2962
|
-
const skillExtraction = extractSkillFrontMatter(((await client.getPrompt(promptInfo.name)).messages || []).map((m) => {
|
|
2963
|
-
const content = m.content;
|
|
2964
|
-
if (typeof content === "string") return content;
|
|
2965
|
-
if (content && typeof content === "object" && "text" in content) return String(content.text);
|
|
2966
|
-
return "";
|
|
2967
|
-
}).join("\n"));
|
|
2968
|
-
if (skillExtraction) return {
|
|
2969
|
-
serverName: client.serverName,
|
|
2970
|
-
promptName: promptInfo.name,
|
|
2971
|
-
skill: skillExtraction.skill
|
|
2972
|
-
};
|
|
2973
|
-
return null;
|
|
2974
|
-
} catch (error) {
|
|
2975
|
-
fetchPromptFailures++;
|
|
2976
|
-
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to fetch prompt '${promptInfo.name}' from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2977
|
-
return null;
|
|
2978
|
-
}
|
|
2979
|
-
}));
|
|
2980
|
-
for (const result of promptResults) if (result) detectedSkills.push(result);
|
|
2981
|
-
} catch (error) {
|
|
2982
|
-
listPromptsFailures++;
|
|
2983
|
-
console.error(`${LOG_PREFIX_SKILL_DETECTION} Failed to list prompts from ${client.serverName}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2984
|
-
}
|
|
2985
|
-
return detectedSkills;
|
|
2986
|
-
}))).flat();
|
|
2987
|
-
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).`);
|
|
2988
|
-
this.autoDetectedSkillsCache = autoDetectedSkills;
|
|
2989
|
-
return autoDetectedSkills;
|
|
2990
|
-
}
|
|
2991
|
-
/**
|
|
2992
2932
|
* Collects skills derived from prompt configurations across all connected MCP servers.
|
|
2993
2933
|
* Includes both explicitly configured prompts and auto-detected skills from front-matter.
|
|
2994
2934
|
*
|
|
@@ -3197,7 +3137,20 @@ var DescribeToolsTool = class DescribeToolsTool {
|
|
|
3197
3137
|
if (serverName) {
|
|
3198
3138
|
const serverTools = serverToolsMap.get(serverName);
|
|
3199
3139
|
if (!serverTools) {
|
|
3200
|
-
|
|
3140
|
+
try {
|
|
3141
|
+
const tool = (await (await this.clientManager.ensureConnected(serverName)).listTools()).find((t) => t.name === actualToolName);
|
|
3142
|
+
if (tool) result.tools.push({
|
|
3143
|
+
server: serverName,
|
|
3144
|
+
tool: {
|
|
3145
|
+
name: tool.name,
|
|
3146
|
+
description: tool.description,
|
|
3147
|
+
inputSchema: tool.inputSchema
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
else result.notFound = requestedName;
|
|
3151
|
+
} catch {
|
|
3152
|
+
result.notFound = requestedName;
|
|
3153
|
+
}
|
|
3201
3154
|
return result;
|
|
3202
3155
|
}
|
|
3203
3156
|
const tool = serverTools.find((t) => t.name === actualToolName);
|
|
@@ -4975,4 +4928,4 @@ const TRANSPORT_MODE = {
|
|
|
4975
4928
|
SSE: "sse"
|
|
4976
4929
|
};
|
|
4977
4930
|
//#endregion
|
|
4978
|
-
export { DefinitionsCacheService as C, version as D, ConfigFetcherService as E, createProxyLogger as S, findConfigFile as T, DescribeToolsTool as _, createProxyContainer as a, RuntimeStateService as b, createStdioHttpTransportHandler as c, StdioHttpTransportHandler as d, StdioTransportHandler as f, SearchListToolsTool as g, UseToolTool as h, createHttpTransportHandler as i, createStdioTransportHandler as l, HttpTransportHandler as m, createServer$1 as n, createProxyIoCContainer as o, SseTransportHandler as p, createSessionServer as r, createSseTransportHandler as s, TRANSPORT_MODE as t, initializeSharedServices as u,
|
|
4931
|
+
export { DefinitionsCacheService as C, version as D, ConfigFetcherService as E, createProxyLogger as S, findConfigFile as T, DescribeToolsTool as _, createProxyContainer as a, RuntimeStateService as b, createStdioHttpTransportHandler as c, StdioHttpTransportHandler as d, StdioTransportHandler as f, SearchListToolsTool as g, UseToolTool as h, createHttpTransportHandler as i, createStdioTransportHandler as l, HttpTransportHandler as m, createServer$1 as n, createProxyIoCContainer as o, SseTransportHandler as p, createSessionServer as r, createSseTransportHandler as s, TRANSPORT_MODE as t, initializeSharedServices as u, StopServerService as v, generateServerId as w, McpClientManagerService as x, SkillService as y };
|