From 44e0309fd46287393903da90724ec7542ffa558c Mon Sep 17 00:00:00 2001 From: samanhappy Date: Fri, 31 Oct 2025 21:56:43 +0800 Subject: [PATCH] Feat: Enhance package cache for stdio servers (#400) --- Dockerfile | 22 ++++++- entrypoint.sh | 22 +++++++ src/services/mcpService.ts | 120 ++++++++++++++++++++++++++++++++++++- 3 files changed, 159 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0d89081..200d539 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,9 +9,25 @@ RUN apt-get update && apt-get install -y curl gnupg git \ RUN npm install -g pnpm -ENV PNPM_HOME=/usr/local/share/pnpm -ENV PATH=$PNPM_HOME:$PATH -RUN mkdir -p $PNPM_HOME && \ +ENV MCP_DATA_DIR=/app/data +ENV MCP_SERVERS_DIR=$MCP_DATA_DIR/servers +ENV MCP_NPM_DIR=$MCP_SERVERS_DIR/npm +ENV MCP_PYTHON_DIR=$MCP_SERVERS_DIR/python +ENV PNPM_HOME=$MCP_DATA_DIR/pnpm +ENV NPM_CONFIG_PREFIX=$MCP_DATA_DIR/npm-global +ENV NPM_CONFIG_CACHE=$MCP_DATA_DIR/npm-cache +ENV UV_TOOL_DIR=$MCP_DATA_DIR/uv/tools +ENV UV_CACHE_DIR=$MCP_DATA_DIR/uv/cache +ENV PATH=$PNPM_HOME:$NPM_CONFIG_PREFIX/bin:$UV_TOOL_DIR/bin:$PATH +RUN mkdir -p \ + $PNPM_HOME \ + $NPM_CONFIG_PREFIX/bin \ + $NPM_CONFIG_PREFIX/lib/node_modules \ + $NPM_CONFIG_CACHE \ + $UV_TOOL_DIR \ + $UV_CACHE_DIR \ + $MCP_NPM_DIR \ + $MCP_PYTHON_DIR && \ pnpm add -g @amap/amap-maps-mcp-server @playwright/mcp@latest tavily-mcp@latest @modelcontextprotocol/server-github @modelcontextprotocol/server-slack ARG INSTALL_EXT=false diff --git a/entrypoint.sh b/entrypoint.sh index d4f7382..7e077df 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,27 @@ #!/bin/bash +DATA_DIR=${MCP_DATA_DIR:-/app/data} +SERVERS_DIR=${MCP_SERVERS_DIR:-$DATA_DIR/servers} +NPM_SERVER_DIR=${MCP_NPM_DIR:-$SERVERS_DIR/npm} +PYTHON_SERVER_DIR=${MCP_PYTHON_DIR:-$SERVERS_DIR/python} +PNPM_HOME=${PNPM_HOME:-$DATA_DIR/pnpm} +NPM_CONFIG_PREFIX=${NPM_CONFIG_PREFIX:-$DATA_DIR/npm-global} +NPM_CONFIG_CACHE=${NPM_CONFIG_CACHE:-$DATA_DIR/npm-cache} +UV_TOOL_DIR=${UV_TOOL_DIR:-$DATA_DIR/uv/tools} +UV_CACHE_DIR=${UV_CACHE_DIR:-$DATA_DIR/uv/cache} + +mkdir -p \ + "$PNPM_HOME" \ + "$NPM_CONFIG_PREFIX/bin" \ + "$NPM_CONFIG_PREFIX/lib/node_modules" \ + "$NPM_CONFIG_CACHE" \ + "$UV_TOOL_DIR" \ + "$UV_CACHE_DIR" \ + "$NPM_SERVER_DIR" \ + "$PYTHON_SERVER_DIR" + +export PATH="$PNPM_HOME:$NPM_CONFIG_PREFIX/bin:$UV_TOOL_DIR/bin:$PATH" + NPM_REGISTRY=${NPM_REGISTRY:-https://registry.npmjs.org/} echo "Setting npm registry to ${NPM_REGISTRY}" npm config set registry "$NPM_REGISTRY" diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 85a6729..801e4da 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -1,4 +1,6 @@ +import fs from 'fs'; import os from 'os'; +import path from 'path'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, @@ -31,6 +33,77 @@ const servers: { [sessionId: string]: Server } = {}; const serverDao = getServerDao(); +const ensureDirExists = (dir: string | undefined): string => { + if (!dir) { + throw new Error('Directory path is undefined'); + } + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + return dir; +}; + +const getDataRootDir = (): string => { + return ensureDirExists(process.env.MCP_DATA_DIR || path.join(process.cwd(), 'data')); +}; + +const getServersStorageRoot = (): string => { + return ensureDirExists(process.env.MCP_SERVERS_DIR || path.join(getDataRootDir(), 'servers')); +}; + +const getNpmBaseDir = (): string => { + return ensureDirExists(process.env.MCP_NPM_DIR || path.join(getServersStorageRoot(), 'npm')); +}; + +const getPythonBaseDir = (): string => { + return ensureDirExists( + process.env.MCP_PYTHON_DIR || path.join(getServersStorageRoot(), 'python'), + ); +}; + +const getNpmCacheDir = (): string => { + return ensureDirExists(process.env.NPM_CONFIG_CACHE || path.join(getDataRootDir(), 'npm-cache')); +}; + +const getNpmPrefixDir = (): string => { + const dir = ensureDirExists( + process.env.NPM_CONFIG_PREFIX || path.join(getDataRootDir(), 'npm-global'), + ); + ensureDirExists(path.join(dir, 'bin')); + ensureDirExists(path.join(dir, 'lib', 'node_modules')); + return dir; +}; + +const getUvCacheDir = (): string => { + return ensureDirExists(process.env.UV_CACHE_DIR || path.join(getDataRootDir(), 'uv', 'cache')); +}; + +const getUvToolDir = (): string => { + const dir = ensureDirExists(process.env.UV_TOOL_DIR || path.join(getDataRootDir(), 'uv', 'tools')); + ensureDirExists(path.join(dir, 'bin')); + return dir; +}; + +const getServerInstallDir = (serverName: string, kind: 'npm' | 'python'): string => { + const baseDir = kind === 'npm' ? getNpmBaseDir() : getPythonBaseDir(); + return ensureDirExists(path.join(baseDir, serverName)); +}; + +const prependToPath = (currentPath: string, dir: string): string => { + if (!dir) { + return currentPath; + } + const delimiter = path.delimiter; + const segments = currentPath ? currentPath.split(delimiter) : []; + if (segments.includes(dir)) { + return currentPath; + } + return currentPath ? `${dir}${delimiter}${currentPath}` : dir; +}; + +const NODE_COMMANDS = new Set(['npm', 'npx', 'pnpm', 'yarn', 'node', 'bun', 'bunx']); +const PYTHON_COMMANDS = new Set(['uv', 'uvx', 'python', 'pip', 'pip3', 'pipx']); + // Helper function to set up keep-alive ping for SSE connections const setupKeepAlive = (serverInfo: ServerInfo, serverConfig: ServerConfig): void => { // Only set up keep-alive for SSE connections @@ -213,7 +286,7 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig ...(process.env as Record), ...replaceEnvVars(conf.env || {}), }; - env['PATH'] = expandEnvVars(process.env.PATH as string) || ''; + env['PATH'] = expandEnvVars(env['PATH'] || process.env.PATH || ''); const settings = loadSettings(); // Add UV_DEFAULT_INDEX and npm_config_registry if needed @@ -235,9 +308,52 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig env['npm_config_registry'] = settings.systemConfig.install.npmRegistry; } + // Ensure stdio servers use persistent directories under /app/data (or configured override) + let workingDirectory = os.homedir(); + const commandLower = conf.command.toLowerCase(); + + if (NODE_COMMANDS.has(commandLower)) { + const serverDir = getServerInstallDir(name, 'npm'); + workingDirectory = serverDir; + + const npmCacheDir = getNpmCacheDir(); + const npmPrefixDir = getNpmPrefixDir(); + + if (!env['npm_config_cache']) { + env['npm_config_cache'] = npmCacheDir; + } + if (!env['NPM_CONFIG_CACHE']) { + env['NPM_CONFIG_CACHE'] = env['npm_config_cache']; + } + + if (!env['npm_config_prefix']) { + env['npm_config_prefix'] = npmPrefixDir; + } + if (!env['NPM_CONFIG_PREFIX']) { + env['NPM_CONFIG_PREFIX'] = env['npm_config_prefix']; + } + + env['PATH'] = prependToPath(env['PATH'], path.join(env['npm_config_prefix'], 'bin')); + } else if (PYTHON_COMMANDS.has(commandLower)) { + const serverDir = getServerInstallDir(name, 'python'); + workingDirectory = serverDir; + + const uvCacheDir = getUvCacheDir(); + const uvToolDir = getUvToolDir(); + + if (!env['UV_CACHE_DIR']) { + env['UV_CACHE_DIR'] = uvCacheDir; + } + if (!env['UV_TOOL_DIR']) { + env['UV_TOOL_DIR'] = uvToolDir; + } + + env['PATH'] = prependToPath(env['PATH'], path.join(env['UV_TOOL_DIR'], 'bin')); + } + // Expand environment variables in command transport = new StdioClientTransport({ - cwd: os.homedir(), + cwd: workingDirectory, command: conf.command, args: replaceEnvVars(conf.args) as string[], env: env,