maplestory-mcp-server
Version:
Official-style NEXON MapleStory MCP Server for Claude Desktop - Complete character info, union details, guild data, rankings, and game mechanics
254 lines • 9.49 kB
JavaScript
"use strict";
/**
* NEXON MapleStory Open API Client
* Provides methods to interact with NEXON's official MapleStory API
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NexonApiClient = void 0;
const axios_1 = __importDefault(require("axios"));
const winston_1 = require("winston");
const constants_1 = require("./constants");
class NexonApiClient {
client;
logger;
apiKey;
constructor(config) {
this.apiKey = config.apiKey;
// Check if in MCP mode (no port specified)
const isMcpMode = !process.env.MCP_PORT && !process.argv.includes('--port');
this.logger = (0, winston_1.createLogger)({
level: 'info',
format: require('winston').format.combine(require('winston').format.timestamp(), require('winston').format.json()),
silent: isMcpMode,
transports: isMcpMode ? [] : [new (require('winston').transports.Console)()],
});
// Create axios instance with default configuration
this.client = axios_1.default.create({
baseURL: config.baseURL || constants_1.API_CONFIG.BASE_URL,
timeout: config.timeout || constants_1.API_CONFIG.TIMEOUT,
headers: {
[constants_1.HEADERS.AUTHORIZATION]: this.apiKey,
'Content-Type': constants_1.HEADERS.CONTENT_TYPE,
'User-Agent': constants_1.HEADERS.USER_AGENT,
},
});
this.setupInterceptors();
}
setupInterceptors() {
// Request interceptor
this.client.interceptors.request.use((config) => {
this.logger.info('API Request', {
method: config.method?.toUpperCase(),
url: config.url,
params: config.params,
});
return config;
}, (error) => {
this.logger.error('Request Error', { error: error.message });
return Promise.reject(error);
});
// Response interceptor
this.client.interceptors.response.use((response) => {
this.logger.info('API Response', {
status: response.status,
url: response.config.url,
dataSize: JSON.stringify(response.data).length,
});
return response;
}, (error) => {
this.logger.error('Response Error', {
status: error.response?.status,
url: error.config?.url,
message: error.message,
data: error.response?.data,
});
// Transform error to our standard format
const apiError = {
error: {
name: this.getErrorName(error.response?.status),
message: this.getErrorMessage(error.response?.status, error.response?.data),
},
};
return Promise.reject(apiError);
});
}
getErrorName(status) {
switch (status) {
case constants_1.HTTP_STATUS.UNAUTHORIZED:
return 'UNAUTHORIZED';
case constants_1.HTTP_STATUS.FORBIDDEN:
return 'FORBIDDEN';
case constants_1.HTTP_STATUS.NOT_FOUND:
return 'NOT_FOUND';
case constants_1.HTTP_STATUS.TOO_MANY_REQUESTS:
return 'RATE_LIMITED';
case constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR:
return 'INTERNAL_ERROR';
case constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE:
return 'SERVICE_UNAVAILABLE';
default:
return 'UNKNOWN_ERROR';
}
}
getErrorMessage(status, data) {
if (data?.message) {
return data.message;
}
switch (status) {
case constants_1.HTTP_STATUS.UNAUTHORIZED:
return constants_1.ERROR_MESSAGES.INVALID_API_KEY;
case constants_1.HTTP_STATUS.TOO_MANY_REQUESTS:
return constants_1.ERROR_MESSAGES.RATE_LIMIT_EXCEEDED;
case constants_1.HTTP_STATUS.NOT_FOUND:
return constants_1.ERROR_MESSAGES.CHARACTER_NOT_FOUND;
default:
return constants_1.ERROR_MESSAGES.UNKNOWN_ERROR;
}
}
async request(endpoint, params) {
const response = await this.client.get(endpoint, { params });
return response.data;
}
// Character API methods
async getCharacterOcid(characterName) {
return this.request(constants_1.ENDPOINTS.CHARACTER.OCID, { character_name: characterName });
}
async getCharacterBasic(ocid, date) {
return this.request(constants_1.ENDPOINTS.CHARACTER.BASIC, { ocid, date });
}
async getCharacterStat(ocid, date) {
return this.request(constants_1.ENDPOINTS.CHARACTER.STAT, { ocid, date });
}
async getCharacterHyperStat(ocid, date) {
return this.request(constants_1.ENDPOINTS.CHARACTER.HYPER_STAT, { ocid, date });
}
async getCharacterPropensity(ocid, date) {
return this.request(constants_1.ENDPOINTS.CHARACTER.PROPENSITY, { ocid, date });
}
async getCharacterAbility(ocid, date) {
return this.request(constants_1.ENDPOINTS.CHARACTER.ABILITY, { ocid, date });
}
async getCharacterItemEquipment(ocid, date) {
return this.request(constants_1.ENDPOINTS.CHARACTER.ITEM_EQUIPMENT, { ocid, date });
}
// Union API methods
async getUnionInfo(ocid, date) {
return this.request(constants_1.ENDPOINTS.UNION.BASIC, { ocid, date });
}
async getUnionRaider(ocid, date) {
return this.request(constants_1.ENDPOINTS.UNION.RAIDER, { ocid, date });
}
// Guild API methods
async getGuildId(guildName, worldName) {
return this.request(constants_1.ENDPOINTS.GUILD.ID, {
guild_name: guildName,
world_name: worldName,
});
}
async getGuildBasic(oguildId, date) {
return this.request(constants_1.ENDPOINTS.GUILD.BASIC, { oguild_id: oguildId, date });
}
// Ranking API methods
async getOverallRanking(worldName, worldType, className, ocid, page, date) {
return this.request(constants_1.ENDPOINTS.RANKING.OVERALL, {
world_name: worldName,
world_type: worldType,
class: className,
ocid,
page,
date,
});
}
async getUnionRanking(worldName, ocid, page, date) {
return this.request(constants_1.ENDPOINTS.RANKING.UNION, {
world_name: worldName,
ocid,
page,
date,
});
}
async getGuildRanking(worldName, rankingType, guildName, page, date) {
return this.request(constants_1.ENDPOINTS.RANKING.GUILD, {
world_name: worldName,
ranking_type: rankingType,
guild_name: guildName,
page,
date,
});
}
// Convenience methods
async getCharacterFullInfo(characterName, date) {
try {
// First get OCID
const { ocid } = await this.getCharacterOcid(characterName);
// Then fetch all character information in parallel
const [basic, stat, hyperStat, propensity, ability, equipment] = await Promise.all([
this.getCharacterBasic(ocid, date),
this.getCharacterStat(ocid, date),
this.getCharacterHyperStat(ocid, date),
this.getCharacterPropensity(ocid, date),
this.getCharacterAbility(ocid, date),
this.getCharacterItemEquipment(ocid, date),
]);
return {
ocid,
basic,
stat,
hyperStat,
propensity,
ability,
equipment,
};
}
catch (error) {
this.logger.error('Error fetching character full info', {
characterName,
error,
});
throw error;
}
}
async getGuildFullInfo(guildName, worldName, date) {
try {
// First get guild ID
const { oguild_id } = await this.getGuildId(guildName, worldName);
// Then fetch guild information
const basic = await this.getGuildBasic(oguild_id, date);
return {
oguild_id,
basic,
};
}
catch (error) {
this.logger.error('Error fetching guild full info', {
guildName,
worldName,
error,
});
throw error;
}
}
// Health check method
async healthCheck() {
try {
// Try to get ranking data as a health check
await this.getOverallRanking(undefined, undefined, undefined, undefined, 1);
return {
status: 'healthy',
timestamp: new Date().toISOString(),
};
}
catch (error) {
this.logger.error('Health check failed', { error });
return {
status: 'unhealthy',
timestamp: new Date().toISOString(),
};
}
}
}
exports.NexonApiClient = NexonApiClient;
//# sourceMappingURL=nexon-client.js.map