Welcome to the first chapter of the MCP (Model Context Protocol) project tutorial!
Before we can send messages, handle security, or manage connections, we need to know who we are talking to. We need a map of available servers.
This chapter covers the Configuration Hierarchy & Loading system. Think of this as the "Registry" or "Phonebook" of the application. It decides which tools and servers are available to the user at any given moment.
Imagine you are a developer working on two things:
You don't want your work database tools cluttering your hobby project, and you definitely don't want your personal API keys leaking into work servers.
MCP solves this using a Cascading Configuration system, very similar to how CSS (Cascading Style Sheets) works. We have different "scopes" or layers where settings can live.
We will walk through how the system loads configurations to answer a simple question: "Which MCP servers should be running right now?"
The system looks for configuration files in a specific order. The more specific scope overrides the general one (unless Enterprise policy locks it down).
~/.config/mcp.json). available in all your projects.
What does a configuration look like? It's a JSON object defining servers. A server usually needs a command to run (like node server.js) or a URL to connect to.
Here is a simplified look at the type definition from types.ts:
// types.ts
export type McpServerConfig =
| { type: 'stdio', command: string, args: string[] }
| { type: 'sse', url: string }
// ... other types like websocket, http, etc.
export type ScopedMcpServerConfig = McpServerConfig & {
scope: 'local' | 'user' | 'project' | 'enterprise' // ...
}
When the application starts, it doesn't just read one file. It gathers data from everywhere, cleans it up, and decides who wins.
Let's look under the hood at config.ts, the brain of this operation.
The main entry point is getClaudeCodeMcpConfigs. It orchestrates the gathering of configurations.
// config.ts
export async function getClaudeCodeMcpConfigs(
dynamicServers = {},
extraDedupTargets = Promise.resolve({})
) {
// 1. Enterprise has exclusive control if it exists
if (doesEnterpriseMcpConfigExist()) {
const { servers } = getMcpConfigsByScope('enterprise');
return { servers: filterByPolicy(servers), errors: [] };
}
// 2. Otherwise, load other scopes
const { servers: userServers } = getMcpConfigsByScope('user');
const { servers: projectServers } = getMcpConfigsByScope('project');
// ... load local and plugins ...
}
Explanation: The code first checks for the "Boss" (Enterprise config). If it exists, it ignores everything else. If not, it proceeds to load User and Project settings.
Configuration files often contain secrets like ${API_KEY}. We can't use them literally; we must "expand" them using environment variables.
// envExpansion.ts
export function expandEnvVarsInString(value: string) {
// Matches syntax like ${VAR_NAME} or ${VAR:-default}
return value.replace(/\$\{([^}]+)\}/g, (match, varContent) => {
const [varName, defaultValue] = varContent.split(':-', 2);
const envValue = process.env[varName];
return envValue ?? defaultValue ?? match; // Return match if missing
});
}
Explanation: This utility takes a string from the config file. It looks for the ${...} pattern. If it finds one, it looks up that key in process.env. If the variable is missing, it keeps the original string (so we can report an error later).
What if a plugin provides a "Github" tool, but you also manually added a "Github" tool in your config? We don't want to run both. The system uses Signatures to detect duplicates.
// config.ts
export function getMcpServerSignature(config: McpServerConfig): string | null {
const cmd = getServerCommandArray(config);
if (cmd) {
// Two servers are duplicates if they run the exact same command
return `stdio:${jsonStringify(cmd)}`;
}
const url = getServerUrl(config);
if (url) {
// Two servers are duplicates if they hit the same URL
return `url:${unwrapCcrProxyUrl(url)}`;
}
return null;
}
Explanation: We generate a unique ID (signature) for every server. If Server A and Server B have the same signature, the system knows they are actually the same tool and will only load the most specific one (Manual config > Plugin).
Sometimes, servers aren't files on disk. They are permissions granted by your organization on Claude.ai.
// claudeai.ts
export const fetchClaudeAIMcpConfigsIfEligible = memoize(async () => {
const tokens = getClaudeAIOAuthTokens();
// We need specific permissions (scopes) to read these servers
if (!tokens.scopes?.includes('user:mcp_servers')) {
return {};
}
// Fetch from the API
const response = await axios.get(`${baseUrl}/v1/mcp_servers`, { ... });
// Process and return config objects...
});
Explanation: This function checks if the user is logged in and has permission. It calls the remote API to see if the organization has assigned any servers to this user remotely.
Before any of this data is used, it must be validated. We use a library called zod to ensure the JSON we read actually matches the shape we expect.
// config.ts
export function parseMcpConfig(params: { configObject: unknown, ... }) {
// Validate against the schema defined in types.ts
const schemaResult = McpJsonConfigSchema().safeParse(params.configObject);
if (!schemaResult.success) {
return { config: null, errors: formatErrors(schemaResult.error) };
}
// If valid, expand variables and return
// ...
}
Explanation: If a user makes a typo in their .mcp.json file (like missing a brace or using a number where a string is required), safeParse will fail, and the system will report a friendly error instead of crashing.
In this chapter, we learned:
${KEY}) are swapped with real environment variables.Now that we know which servers we want to talk to, we need to ensure we have permission to talk to them.
Next Chapter: Authentication & Security (OAuth/XAA)
Generated by Code IQ