Skip to content

Commit 5441329

Browse files
authored
feat(config): add MCP initialization config handling and prioritization (#12)
Introduce a handler extracting configuration from MCP client info during initialization, enabling dynamic config setup based on client-provided data. Update configuration loader to prioritize MCP init config over environment variables and other sources, ensuring runtime flexibility and seamless integration with MCP clients. Add comprehensive tests verifying MCP config precedence, boolean parsing, nested config handling, and hostname extraction. Fixes #7
1 parent 30c72ed commit 5441329

3 files changed

Lines changed: 189 additions & 8 deletions

File tree

src/index.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export async function startServer(
5353
title: "Lokalise MCP Server",
5454
});
5555

56+
// Set up MCP initialization handler to extract configuration
57+
setupMcpInitializationHandler(serverInstance);
58+
5659
serverLogger.info("Registering MCP tools, resources, and prompts...");
5760
await registerAllTools(serverInstance);
5861
await registerAllResources(serverInstance);
@@ -219,3 +222,56 @@ function setupGracefulShutdown() {
219222
});
220223
});
221224
}
225+
226+
/**
227+
* Set up MCP initialization handler to extract configuration from clientInfo
228+
*/
229+
function setupMcpInitializationHandler(server: McpServer): void {
230+
const initLogger = Logger.forContext(
231+
"index.ts",
232+
"setupMcpInitializationHandler",
233+
);
234+
235+
// Store the original oninitialized callback
236+
const originalOnInitialized = server.server.oninitialized;
237+
238+
// Set up a custom oninitialized callback to extract configuration
239+
server.server.oninitialized = () => {
240+
initLogger.debug("MCP initialization completed");
241+
242+
// Try to extract configuration from the client info
243+
const clientVersion = server.server.getClientVersion();
244+
if (clientVersion && typeof clientVersion === "object") {
245+
// Look for configuration in clientInfo - Smithery typically passes config here
246+
const configData: Record<string, unknown> = {};
247+
248+
// Extract all non-standard fields from clientInfo as potential config
249+
for (const [key, value] of Object.entries(clientVersion)) {
250+
if (!["name", "version", "title"].includes(key)) {
251+
configData[key] = value;
252+
}
253+
}
254+
255+
// Also check for explicit config object
256+
if (
257+
"config" in clientVersion &&
258+
typeof clientVersion.config === "object" &&
259+
clientVersion.config
260+
) {
261+
Object.assign(configData, clientVersion.config);
262+
}
263+
264+
if (Object.keys(configData).length > 0) {
265+
initLogger.info("Extracted configuration from MCP initialization", {
266+
configKeys: Object.keys(configData),
267+
});
268+
config.setMcpInitConfig(configData);
269+
}
270+
}
271+
272+
// Call the original callback if it existed
273+
if (originalOnInitialized) {
274+
originalOnInitialized();
275+
}
276+
};
277+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { beforeEach, describe, expect, test } from "@jest/globals";
2+
import { config } from "./config.util.js";
3+
4+
describe("ConfigLoader MCP Initialization", () => {
5+
beforeEach(() => {
6+
// Reset the config instance for each test
7+
// biome-ignore lint/suspicious/noExplicitAny: Needed for testing private properties
8+
(config as any).configLoaded = false;
9+
// biome-ignore lint/suspicious/noExplicitAny: Needed for testing private properties
10+
(config as any).mcpInitConfig = {};
11+
12+
// Clear any environment variables that might interfere
13+
delete process.env.TEST_KEY;
14+
delete process.env.LOKALISE_API_KEY;
15+
});
16+
17+
test("should prioritize MCP init config over environment variables", () => {
18+
// Set up environment variable
19+
process.env.TEST_KEY = "env_value";
20+
21+
// Set up MCP init config (should have higher priority)
22+
config.setMcpInitConfig({ TEST_KEY: "mcp_value" });
23+
24+
// Load config from all sources
25+
config.load();
26+
27+
// MCP config should take precedence
28+
expect(config.get("TEST_KEY")).toBe("mcp_value");
29+
});
30+
31+
test("should fall back to environment variables when MCP config is not set", () => {
32+
// Set up environment variable
33+
process.env.TEST_KEY = "env_value";
34+
35+
// Don't set MCP config for this key
36+
config.setMcpInitConfig({});
37+
38+
// Load config from all sources
39+
config.load();
40+
41+
// Should fall back to environment variable
42+
expect(config.get("TEST_KEY")).toBe("env_value");
43+
});
44+
45+
test("should return default value when key is not found in any source", () => {
46+
// Don't set the key anywhere
47+
config.setMcpInitConfig({});
48+
config.load();
49+
50+
// Should return the default value
51+
expect(config.get("NONEXISTENT_KEY", "default_value")).toBe(
52+
"default_value",
53+
);
54+
});
55+
56+
test("should handle boolean values from MCP config", () => {
57+
// Set up MCP init config with boolean-like values
58+
config.setMcpInitConfig({
59+
BOOL_TRUE: "true",
60+
BOOL_FALSE: "false",
61+
BOOL_INVALID: "invalid",
62+
});
63+
64+
config.load();
65+
66+
expect(config.getBoolean("BOOL_TRUE")).toBe(true);
67+
expect(config.getBoolean("BOOL_FALSE")).toBe(false);
68+
expect(config.getBoolean("BOOL_INVALID")).toBe(false);
69+
});
70+
71+
test("should handle nested config object in MCP init", () => {
72+
// Simulate what Smithery might pass
73+
const mcpConfig = {
74+
LOKALISE_API_KEY: "test_api_key",
75+
LOKALISE_API_HOSTNAME: "https://api.example.com/api2/",
76+
DEBUG: "true",
77+
};
78+
79+
config.setMcpInitConfig(mcpConfig);
80+
config.load();
81+
82+
expect(config.get("LOKALISE_API_KEY")).toBe("test_api_key");
83+
expect(config.get("LOKALISE_API_HOSTNAME")).toBe(
84+
"https://api.example.com/api2/",
85+
);
86+
expect(config.getBoolean("DEBUG")).toBe(true);
87+
});
88+
89+
test("should extract hostname correctly from MCP config", () => {
90+
config.setMcpInitConfig({
91+
LOKALISE_API_HOSTNAME: "https://api.stage.lokalise.cloud/api2/",
92+
});
93+
94+
config.load();
95+
96+
expect(config.getLokaliseHostname()).toBe("stage.lokalise.cloud");
97+
});
98+
});

src/shared/utils/config.util.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { Logger } from "./logger.util.js";
66

77
/**
88
* Configuration loader that handles multiple sources with priority:
9-
* 1. Direct ENV pass (process.env)
10-
* 2. .env file in project root
11-
* 3. Global config file at $HOME/.mcp/configs.json
9+
* 1. MCP initialization config (from clientInfo)
10+
* 2. Direct ENV pass (process.env)
11+
* 3. .env file in project root
12+
* 4. Global config file at $HOME/.mcp/configs.json
1213
*/
1314
class ConfigLoader {
1415
private packageName: string;
1516
private configLoaded = false;
17+
private mcpInitConfig: Record<string, unknown> = {};
1618

1719
/**
1820
* Create a new ConfigLoader instance
@@ -22,6 +24,22 @@ class ConfigLoader {
2224
this.packageName = packageName;
2325
}
2426

27+
/**
28+
* Set configuration from MCP initialization (highest priority)
29+
* @param config Configuration object from MCP clientInfo
30+
*/
31+
setMcpInitConfig(config: Record<string, unknown>): void {
32+
const methodLogger = Logger.forContext(
33+
"utils/config.util.ts",
34+
"setMcpInitConfig",
35+
);
36+
37+
this.mcpInitConfig = { ...config };
38+
methodLogger.debug("MCP initialization configuration set", {
39+
keysCount: Object.keys(config).length,
40+
});
41+
}
42+
2543
/**
2644
* Load configuration from all sources with proper priority
2745
*/
@@ -35,15 +53,18 @@ class ConfigLoader {
3553

3654
methodLogger.debug("Loading configuration...");
3755

38-
// Priority 3: Load from global config file
56+
// Priority 4: Load from global config file
3957
this.loadFromGlobalConfig();
4058

41-
// Priority 2: Load from .env file
59+
// Priority 3: Load from .env file
4260
this.loadFromEnvFile();
4361

44-
// Priority 1: Direct ENV pass is already in process.env
62+
// Priority 2: Direct ENV pass is already in process.env
4563
// No need to do anything as it already has highest priority
4664

65+
// Priority 1: MCP init config is handled in get() method
66+
// No need to set process.env as it might conflict with existing values
67+
4768
this.configLoaded = true;
4869
methodLogger.debug("Configuration loaded successfully");
4970
}
@@ -92,7 +113,7 @@ class ConfigLoader {
92113

93114
// Determine the potential keys for the current package
94115
const shortKey = "boilerplate"; // Project-specific short key
95-
const fullPackageName = this.packageName; // e.g., '@aashari/boilerplate-mcp-server'
116+
const fullPackageName = this.packageName; // e.g., 'lokalise-mcp'
96117
const unscopedPackageName =
97118
fullPackageName.split("/")[1] || fullPackageName; // e.g., 'boilerplate-mcp-server'
98119

@@ -145,6 +166,12 @@ class ConfigLoader {
145166
* @returns The configuration value or the default value
146167
*/
147168
get(key: string, defaultValue?: string): string | undefined {
169+
// Priority 1: MCP initialization config
170+
if (this.mcpInitConfig[key] !== undefined) {
171+
return String(this.mcpInitConfig[key]);
172+
}
173+
174+
// Priority 2: Environment variables
148175
return process.env[key] || defaultValue;
149176
}
150177

@@ -196,4 +223,4 @@ class ConfigLoader {
196223
}
197224

198225
// Create and export a singleton instance with the package name from package.json
199-
export const config = new ConfigLoader("@aashari/boilerplate-mcp-server");
226+
export const config = new ConfigLoader("lokalise-mcp");

0 commit comments

Comments
 (0)