mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Feat: Enhance package cache for stdio servers (#400)
This commit is contained in:
22
Dockerfile
22
Dockerfile
@@ -9,9 +9,25 @@ RUN apt-get update && apt-get install -y curl gnupg git \
|
|||||||
|
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
ENV PNPM_HOME=/usr/local/share/pnpm
|
ENV MCP_DATA_DIR=/app/data
|
||||||
ENV PATH=$PNPM_HOME:$PATH
|
ENV MCP_SERVERS_DIR=$MCP_DATA_DIR/servers
|
||||||
RUN mkdir -p $PNPM_HOME && \
|
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
|
pnpm add -g @amap/amap-maps-mcp-server @playwright/mcp@latest tavily-mcp@latest @modelcontextprotocol/server-github @modelcontextprotocol/server-slack
|
||||||
|
|
||||||
ARG INSTALL_EXT=false
|
ARG INSTALL_EXT=false
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
#!/bin/bash
|
#!/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/}
|
NPM_REGISTRY=${NPM_REGISTRY:-https://registry.npmjs.org/}
|
||||||
echo "Setting npm registry to ${NPM_REGISTRY}"
|
echo "Setting npm registry to ${NPM_REGISTRY}"
|
||||||
npm config set registry "$NPM_REGISTRY"
|
npm config set registry "$NPM_REGISTRY"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import {
|
import {
|
||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
@@ -31,6 +33,77 @@ const servers: { [sessionId: string]: Server } = {};
|
|||||||
|
|
||||||
const serverDao = getServerDao();
|
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
|
// Helper function to set up keep-alive ping for SSE connections
|
||||||
const setupKeepAlive = (serverInfo: ServerInfo, serverConfig: ServerConfig): void => {
|
const setupKeepAlive = (serverInfo: ServerInfo, serverConfig: ServerConfig): void => {
|
||||||
// Only set up keep-alive for SSE connections
|
// Only set up keep-alive for SSE connections
|
||||||
@@ -213,7 +286,7 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig
|
|||||||
...(process.env as Record<string, string>),
|
...(process.env as Record<string, string>),
|
||||||
...replaceEnvVars(conf.env || {}),
|
...replaceEnvVars(conf.env || {}),
|
||||||
};
|
};
|
||||||
env['PATH'] = expandEnvVars(process.env.PATH as string) || '';
|
env['PATH'] = expandEnvVars(env['PATH'] || process.env.PATH || '');
|
||||||
|
|
||||||
const settings = loadSettings();
|
const settings = loadSettings();
|
||||||
// Add UV_DEFAULT_INDEX and npm_config_registry if needed
|
// 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;
|
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
|
// Expand environment variables in command
|
||||||
transport = new StdioClientTransport({
|
transport = new StdioClientTransport({
|
||||||
cwd: os.homedir(),
|
cwd: workingDirectory,
|
||||||
command: conf.command,
|
command: conf.command,
|
||||||
args: replaceEnvVars(conf.args) as string[],
|
args: replaceEnvVars(conf.args) as string[],
|
||||||
env: env,
|
env: env,
|
||||||
|
|||||||
Reference in New Issue
Block a user