@kaminaduck/scryfall-mcp-server
Version:
A Model Context Protocol (MCP) server that provides access to the Scryfall API for Magic: The Gathering card data
351 lines • 17.9 kB
JavaScript
/**
* Scryfall MCP Server - Main Entry Point
*
* This module serves as the entry point for the Scryfall MCP server,
* initializing the server and handling the startup process.
*/
import { server, initializeServer } from './server.js';
import { logger } from './logger.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { fileURLToPath } from 'node:url';
import { platform } from 'node:os';
/**
* Validate environment variables and detect unresolved variables that may cause issues.
*/
function validateEnvironmentVariables() {
const criticalVars = ['APPDATA', 'LOCALAPPDATA', 'USERPROFILE'];
if (platform() === 'win32') {
logger.info('[Env] Running on Windows, checking environment variables...');
// Check if running under Claude Desktop
const isClaudeDesktop = process.env['CLAUDE_DESKTOP'] ||
process.cwd().includes('AnthropicClaude') ||
process.env['npm_config_user_agent']?.includes('Claude');
if (isClaudeDesktop) {
logger.warn('[Env] Detected Claude Desktop execution context');
logger.warn('[Env] Known issue: Claude Desktop may not expand Windows environment variables with npx');
}
let hasUnresolvedVars = false;
for (const varName of criticalVars) {
const value = process.env[varName];
if (!value) {
logger.warn(`[Env] Missing Windows environment variable: ${varName}`);
}
else if (value.includes('${')) {
hasUnresolvedVars = true;
logger.error(`[Env] Unresolved environment variable detected: ${varName}=${value}`);
logger.error('[Env] This may cause ENOENT errors. Claude Desktop may not be expanding variables properly.');
}
else {
logger.info(`[Env] ${varName}=${value}`);
}
}
// Provide specific guidance if unresolved variables are detected
if (hasUnresolvedVars) {
logger.error('[Env] === WINDOWS ENVIRONMENT VARIABLE ISSUE DETECTED ===');
logger.error('[Env] Claude Desktop is not expanding environment variables correctly.');
logger.error('[Env] Please try one of these solutions:');
logger.error('[Env] 1. Use global installation: npm install -g @kaminaduck/scryfall-mcp-server');
logger.error('[Env] 2. Use the Windows wrapper: scryfall-mcp-server-windows');
logger.error('[Env] 3. Set explicit paths in Claude Desktop configuration');
logger.error('[Env] See README.md for detailed Windows troubleshooting steps');
logger.error('[Env] ================================================');
}
}
// Check for literal unresolved variables in other environment variables
for (const [key, value] of Object.entries(process.env)) {
if (value && typeof value === 'string' && value.includes('${')) {
logger.warn(`[Env] Potentially unresolved variable in ${key}: ${value}`);
}
}
}
/**
* Detect and log execution context information for troubleshooting.
*/
function detectExecutionContext() {
logger.info(`[Context] Platform: ${platform()}`);
logger.info(`[Context] Node version: ${process.version}`);
logger.info(`[Context] Working directory: ${process.cwd()}`);
logger.info(`[Context] Script path: ${process.argv[1]}`);
// Detect NPX execution
const npmExecPath = process.env['npm_execpath'];
const npmConfigPath = process.env['npm_config_user_config'];
const isNpx = (process.argv[0] && process.argv[0].includes('npx')) ||
(npmExecPath ? npmExecPath.includes('npx') : false) ||
(npmConfigPath ? npmConfigPath.includes('npx') : false);
if (isNpx) {
logger.info('[Context] Detected NPX execution - this may cause environment variable issues on Windows');
if (platform() === 'win32') {
logger.warn('[Context] NPX on Windows with Claude Desktop is known to cause ${APPDATA} expansion issues');
logger.warn('[Context] Consider using global installation or Windows wrapper script');
}
}
// Detect Claude Desktop execution context
const isClaudeDesktop = process.env['CLAUDE_DESKTOP'] ||
process.cwd().includes('AnthropicClaude') ||
process.env['npm_execpath']?.includes('AnthropicClaude') ||
process.env['npm_config_user_agent']?.includes('Claude') ||
// macOS-specific Claude Desktop indicators
process.cwd().includes('com.anthropic.claude') ||
process.env['HOME']?.includes('/Library/Application Support/Claude');
if (isClaudeDesktop) {
logger.info('[Context] Claude Desktop execution detected');
if (platform() === 'win32') {
logger.warn('[Context] Windows + Claude Desktop detected - monitoring for environment variable issues');
}
else if (platform() === 'darwin') {
logger.warn('[Context] macOS + Claude Desktop detected - may have file system restrictions');
}
}
// Log MCP-specific context
const mcpServerName = process.env['MCP_SERVER_NAME'];
if (mcpServerName) {
logger.info(`[Context] MCP Server Name: ${mcpServerName}`);
}
const mcpFileDownloads = process.env['MCP_ENABLE_FILE_DOWNLOADS'];
if (mcpFileDownloads) {
logger.info(`[Context] MCP File Downloads: ${mcpFileDownloads}`);
}
// Additional Windows diagnostics if running under Claude Desktop
if (isClaudeDesktop && platform() === 'win32') {
logger.info('[Context] Performing additional Windows diagnostics...');
// Check for common environment variable issues
const problematicVars = ['APPDATA', 'LOCALAPPDATA', 'USERPROFILE'].filter(varName => {
const value = process.env[varName];
return value && value.includes('${');
});
if (problematicVars.length > 0) {
logger.error(`[Context] Found unresolved environment variables: ${problematicVars.join(', ')}`);
logger.error('[Context] This will likely cause path resolution failures');
}
}
// Additional macOS diagnostics if running under Claude Desktop
if (isClaudeDesktop && platform() === 'darwin') {
logger.info('[Context] Performing additional macOS diagnostics...');
// Check for sandboxing indicators
const sandboxIndicators = [
process.env['APP_SANDBOX_CONTAINER_ID'],
process.env['__CFBundleIdentifier'],
process.cwd().includes('/Library/Containers/'),
process.env['HOME']?.includes('/Library/Containers/')
].filter(Boolean);
if (sandboxIndicators.length > 0) {
logger.warn('[Context] App Sandbox restrictions detected - file access may be limited');
logger.warn('[Context] Consider setting SCRYFALL_DATA_DIR to an accessible location');
}
// Check for SIP-protected paths
const protectedPaths = ['/System', '/usr', '/bin', '/sbin', '/var'];
const cwd = process.cwd();
if (protectedPaths.some(path => cwd.startsWith(path))) {
logger.warn('[Context] Working directory is in a SIP-protected location');
}
}
}
async function main() {
let serverInitialized = false;
let transportConnected = false;
try {
logger.info('[Setup] Starting Scryfall MCP server...');
// Validate environment and detect issues early
try {
validateEnvironmentVariables();
detectExecutionContext();
}
catch (envError) {
logger.warn('[Setup] Environment validation warnings:', envError);
// Continue despite environment warnings
}
// Platform-specific initialization checks
if (platform() === 'win32') {
logger.info('[Setup] Performing Windows-specific initialization checks...');
// Check for common Windows path issues
try {
const testPath = process.env['APPDATA'] || 'C:\\Users\\Default\\AppData\\Roaming';
logger.info(`[Setup] Windows path test using: ${testPath}`);
}
catch (pathError) {
logger.error('[Setup] Windows path resolution issue detected:', pathError);
}
}
else if (platform() === 'darwin') {
logger.info('[Setup] Performing macOS-specific initialization checks...');
// Check for common macOS path issues
try {
const homePath = process.env['HOME'] || '/tmp';
logger.info(`[Setup] macOS home path: ${homePath}`);
// Check if we're in a sandboxed environment
if (homePath.includes('/Library/Containers/')) {
logger.warn('[Setup] Sandboxed environment detected - file access may be restricted');
}
}
catch (pathError) {
logger.error('[Setup] macOS path resolution issue detected:', pathError);
}
}
// Initialize server before transport connection to prevent initialization failures
// from causing transport disconnection
logger.info('[Setup] Initializing server components...');
try {
await initializeServer();
serverInitialized = true;
logger.info('[Setup] Server initialization completed successfully');
}
catch (initError) {
logger.warn('[Setup] Server initialization encountered issues:', initError.message);
// Determine if we can continue without full initialization
const criticalErrors = ['MODULE_NOT_FOUND', 'ERR_INVALID_ARG_TYPE', 'EACCES'];
if (initError.code && criticalErrors.includes(initError.code)) {
logger.error('[Setup] Critical initialization error - cannot continue');
throw initError;
}
logger.info('[Setup] Continuing with limited functionality (API-only mode)');
// Server can still provide basic functionality without full initialization
}
// Create stdio transport for MCP communication
logger.info('[Setup] Creating MCP transport...');
const transport = new StdioServerTransport();
// Connect the server to the transport with error handling
logger.info('[Setup] Connecting to MCP transport...');
try {
await server.connect(transport);
transportConnected = true;
logger.info('[Setup] ✓ Scryfall MCP server is running and ready for requests');
}
catch (transportError) {
logger.error('[Setup] Failed to connect to MCP transport:', transportError);
throw transportError;
}
// Handle process termination gracefully
process.on('SIGINT', async () => {
logger.info('[Setup] Received SIGINT, shutting down gracefully...');
try {
await server.close();
}
catch (closeError) {
logger.error('[Setup] Error during graceful shutdown:', closeError);
}
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('[Setup] Received SIGTERM, shutting down gracefully...');
try {
await server.close();
}
catch (closeError) {
logger.error('[Setup] Error during graceful shutdown:', closeError);
}
process.exit(0);
});
}
catch (error) {
logger.error('[Setup] Failed to start Scryfall MCP server:', error);
// Enhanced error details with platform-specific diagnostics
const errorDetails = {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
name: error instanceof Error ? error.name : 'Unknown',
platform: platform(),
nodeVersion: process.version,
workingDirectory: process.cwd(),
serverInitialized,
transportConnected
};
// Add Windows-specific error information
if (platform() === 'win32') {
errorDetails.windowsEnvironment = {
APPDATA: process.env['APPDATA'] || 'NOT_SET',
LOCALAPPDATA: process.env['LOCALAPPDATA'] || 'NOT_SET',
USERPROFILE: process.env['USERPROFILE'] || 'NOT_SET'
};
// Check for common Windows error patterns
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('ENOENT')) {
logger.error('[Setup] ENOENT error detected - this is likely a path resolution issue on Windows');
logger.error('[Setup] Check if environment variables like ${APPDATA} are being resolved properly');
logger.error('[Setup] Try: npm install -g @kaminaduck/scryfall-mcp-server');
}
if (errorMessage.includes('EACCES')) {
logger.error('[Setup] EACCES error detected - this is likely a permissions issue on Windows');
logger.error('[Setup] Check if the process has write permissions to the target directory');
}
}
// Add macOS-specific error information
if (platform() === 'darwin') {
errorDetails.macOSEnvironment = {
HOME: process.env['HOME'] || 'NOT_SET',
USER: process.env['USER'] || 'NOT_SET',
TMPDIR: process.env['TMPDIR'] || 'NOT_SET',
XDG_CACHE_HOME: process.env['XDG_CACHE_HOME'] || 'NOT_SET'
};
// Check for common macOS error patterns
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('ENOENT')) {
logger.error('[Setup] ENOENT error detected - this is likely a path access issue on macOS');
logger.error('[Setup] Claude Desktop may have restricted file system access');
logger.error('[Setup] Try setting: export SCRYFALL_DATA_DIR="$HOME/Documents/scryfall_mcp"');
}
if (errorMessage.includes('EACCES')) {
logger.error('[Setup] EACCES error detected - this is likely a permissions issue on macOS');
logger.error('[Setup] This may be due to App Sandbox or SIP restrictions');
logger.error('[Setup] Try using a user-writable directory like ~/Documents or /tmp');
}
if (errorMessage.includes('EROFS')) {
logger.error('[Setup] EROFS error detected - attempting to write to a read-only file system');
logger.error('[Setup] Set SCRYFALL_DATA_DIR to a writable location');
}
}
logger.error('[Setup] Error details:', errorDetails);
// Provide helpful next steps
logger.error('[Setup] === TROUBLESHOOTING STEPS ===');
logger.error('[Setup] 1. Check the error message above for specific issues');
logger.error('[Setup] 2. Try setting SCRYFALL_DATA_DIR environment variable to a writable directory');
logger.error('[Setup] 3. For persistent issues, see the README.md troubleshooting section');
logger.error('[Setup] 4. Report issues at: https://github.com/KaminaDuck/scryfall-mcp/issues');
process.exit(1);
}
}
// Global error handlers to prevent unhandled exceptions from crashing the server
process.on('unhandledRejection', (reason, promise) => {
logger.error('[Global] Unhandled promise rejection:', reason);
logger.error('[Global] Promise that was rejected:', promise);
// Provide platform-specific guidance for common rejection causes
if (reason && typeof reason === 'object' && 'code' in reason) {
const errorCode = reason.code;
if (errorCode === 'ENOENT' && platform() === 'darwin') {
logger.error('[Global] File/directory not found - possible macOS permission issue');
logger.error('[Global] Try setting SCRYFALL_DATA_DIR to an accessible location');
}
else if (errorCode === 'EACCES') {
logger.error('[Global] Permission denied - check file system permissions');
}
}
// Give the logger time to flush before exiting
setTimeout(() => process.exit(1), 100);
});
process.on('uncaughtException', (error) => {
logger.error('[Global] Uncaught exception:', error);
logger.error('[Global] Error stack:', error.stack);
// Provide helpful context for common exceptions
if (error.message.includes('Transport closed unexpectedly')) {
logger.error('[Global] MCP transport closed - this often indicates an initialization failure');
logger.error('[Global] Check the logs above for storage directory or permission errors');
}
else if ('code' in error && error.code === 'ENOENT') {
logger.error('[Global] File or directory not found during operation');
if (platform() === 'darwin') {
logger.error('[Global] On macOS, this may be due to sandbox restrictions');
}
}
// Give the logger time to flush before exiting
setTimeout(() => process.exit(1), 100);
});
// Only run if this file is executed directly (reliable check for npx execution)
const __filename = fileURLToPath(import.meta.url);
if (process.argv[1] === __filename) {
main().catch((error) => {
logger.error('[Setup] Unhandled error in main:', error);
logger.error('[Setup] Error occurred during startup, exiting...');
process.exit(1);
});
}
//# sourceMappingURL=index.js.map