Optimizing Claude Code Context Usage with Selective MCP Loading

MCP servers consume context tokens every session. Learn how a simple shell function saves context by enabling MCP servers only when needed - load Obsidian for blog work, Postgres for database queries, or nothing for quick code reviews

Claude Code added MCP (Model Context Protocol) support back in version 0.8. MCP servers are powerful - they give Claude access to your filesystem, databases, APIs, whatever you connect. But every MCP server you enable eats context window and adds latency to every request.

My initial solution was to add all my MCP configs to the default startup:

1
alias cy="claude --dangerously-skip-permissions --mcp-config ~/.claude/mcp/all-servers.json"

That worked for about two weeks. Then I noticed Claude was slower and I was hitting context limits on medium-sized tasks. Turns out I was loading Obsidian, Postgres, and filesystem MCP servers for sessions where I just needed to review a pull request.

I needed a way to enable MCP servers only when I actually needed them.

The Problem

Every MCP server loaded at startup consumes context tokens. When you run:

1
claude --mcp-config ~/.claude/mcp/all-servers.json

Claude loads the schema and capabilities for every server in that config file. Before you even write your first prompt, you’ve spent maybe 2-5k tokens just on MCP metadata.

For quick tasks - “review this function”, “explain this error” - you don’t need Obsidian integration or database access. But with a static config, you pay that context tax every time.

I had three options:

  1. Always load everything - Waste context, slower responses, hit limits faster
  2. Load nothing by default - Type long commands every time I need MCP
  3. Enable MCP on-demand - Only load what I need for each session

Simple aliases can’t do option 3. They’re just string replacement.

(Looking at this now, option 3 seems obvious. But I genuinely started with option 1 and lived with the slowness for weeks before it annoyed me enough to fix it.)

The Solution

That’s why I built this shell function. It lets me enable MCP servers selectively:

1
2
3
cy                      # No MCP - fast, minimal context
cy --mcp obsidian       # Just Obsidian for blog work
cy --mcp postgres       # Just database for schema queries

The function:

  • Supports --mcp <config_name> to enable specific servers
  • Auto-discovers available MCP configurations
  • Validates configs exist before launching
  • Defaults to no MCP (fast startup, minimal context)
  • Works in both Bash and ZSH

About 45 lines of bash to avoid MCP overhead when you don’t need it.

Implementation

Here’s the complete shell function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
cy() {
    local mcp_config=""
    local claude_args="--dangerously-skip-permissions"

    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case $1 in
            --mcp)
                if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
                    local mcp_name="$2"
                    local mcp_file="$HOME/.claude/mcp/${mcp_name}.json"

                    if [[ -f "$mcp_file" ]]; then
                        mcp_config="--mcp-config $mcp_file"
                        echo "Using MCP config: $mcp_name"
                    else
                        echo "Error: MCP config '$mcp_name' not found at $mcp_file"
                        echo "Available MCP configs:"
                        ls -1 "$HOME/.claude/mcp/"*.json 2>/dev/null | xargs -n1 basename | sed 's/\.json$//' | sed 's/^/  - /'
                        return 1
                    fi
                    shift 2
                else
                    echo "Error: --mcp requires a configuration name"
                    echo "Usage: cy [--mcp <config_name>] [other_claude_args]"
                    echo "Available MCP configs:"
                    ls -1 "$HOME/.claude/mcp/"*.json 2>/dev/null | xargs -n1 basename | sed 's/\.json$//' | sed 's/^/  - /'
                    return 1
                fi
                ;;
            *)
                # Pass through any other arguments to claude
                claude_args="$claude_args $1"
                shift
                ;;
        esac
    done

    # Execute claude with the constructed arguments
    local full_command="claude $claude_args $mcp_config"
    echo "Executing: $full_command"
    eval $full_command
}

(The eval command on line 87 is technically a security risk - it’ll execute whatever string you construct. But since you’re already using --dangerously-skip-permissions and trusting MCP servers with filesystem access, we’re past worrying about that particular horse leaving the barn.)

It turns out this optimization matters more than I expected. Sessions without MCP start noticeably faster - maybe 1-2 seconds. And I’ve stopped hitting context limits on medium-sized refactoring tasks.

How It Works

The function does four things:

Argument parsing: Loops through everything you pass in using while [[ $# -gt 0 ]]

MCP detection: When it sees --mcp, it:

  1. Grabs the config name that follows
  2. Builds the full path: ~/.claude/mcp/<name>.json
  3. Checks if the file actually exists
  4. Adds --mcp-config to the claude command

Error handling: If a config doesn’t exist:

  1. Shows which file it looked for
  2. Lists all available configs in ~/.claude/mcp/
  3. Returns without launching Claude

Pass-through: Any other arguments (--help, --version, etc.) go straight to Claude unchanged

The key is that MCP is opt-in. Default behavior is no MCP servers loaded.

Usage Examples

Basic Usage (Backward Compatible)

1
2
cy --version
# Executing: claude --dangerously-skip-permissions --version

With MCP Configuration

1
2
3
cy --mcp obsidian
# Using MCP config: obsidian
# Executing: claude --dangerously-skip-permissions --mcp-config /home/user/.claude/mcp/obsidian.json

Combined Arguments

1
2
3
cy --mcp exa --help
# Using MCP config: exa
# Executing: claude --dangerously-skip-permissions --help --mcp-config /home/user/.claude/mcp/exa.json

Error Handling

1
2
3
4
5
cy --mcp nonexistent
# Error: MCP config 'nonexistent' not found at /home/user/.claude/mcp/nonexistent.json
# Available MCP configs:
#   - exa
#   - obsidian

Installation

Add the function to your shell configuration file:

For Bash

1
2
3
# Add to ~/.bashrc
echo 'cy() { ... }' >> ~/.bashrc
source ~/.bashrc

For ZSH

1
2
3
# Add to ~/.zshrc
echo 'cy() { ... }' >> ~/.zshrc
source ~/.zshrc

Universal Installation

Since the function works in both shells, you can create a shared file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Create shared function file
cat > ~/.claude_functions.sh << 'EOF'
cy() {
    # [paste the complete function here]
}
EOF

# Source from both shells
echo 'source ~/.claude_functions.sh' >> ~/.bashrc
echo 'source ~/.claude_functions.sh' >> ~/.zshrc

Testing and Validation

First test - did basic usage still work?

1
2
3
$ cy --version
Executing: claude --dangerously-skip-permissions --version
Claude Code 1.2.3

Good. Then I tried an MCP config:

1
2
3
$ cy --mcp obsidian
Using MCP config: obsidian
Executing: claude --dangerously-skip-permissions --mcp-config /home/user/.claude/mcp/obsidian.json

Worked. I also tested the error handling:

1
2
3
4
5
$ cy --mcp nonexistent
Error: MCP config 'nonexistent' not found at /home/user/.claude/mcp/nonexistent.json
Available MCP configs:
  - obsidian
  - postgres

The validation catches typos and shows you what’s available, which has saved me from several “why isn’t this working” moments.

As for the context savings - I haven’t measured precisely, but sessions without MCP definitely start faster. And I’ve noticed I’m not hitting context limits nearly as often on medium-sized refactoring tasks since making this change.

Why This Approach Works

The big wins are performance and context efficiency:

Faster startup: Sessions without MCP launch noticeably faster Context savings: MCP metadata consumes context tokens - when you don’t need MCP capabilities, why pay the cost? Fewer context limit hits: I’ve stopped running into 200k limits on medium refactoring tasks Smart defaults: Most sessions don’t need MCP - code review, quick explanations, simple refactors work fine without it

Secondary benefits:

Auto-discovery: Lists available configs when you mistype a name Validation: Catches typos before launching Claude Flexibility: Easy to add new MCP configs, just drop a JSON file in ~/.claude/mcp/

The function makes it cheap to have many specialized MCP configs (obsidian, postgres, filesystem, api-tools) and load only what you need.

Directory Structure

Here’s what my MCP setup looks like:

1
2
3
4
~/.claude/mcp/
├── obsidian.json    # Obsidian integration
├── exa.json         # File listing with exa
└── other.json       # Additional tools

Each config is a separate MCP server or group of related servers. This granularity means I can load exactly what I need:

  • Writing blog posts → cy --mcp obsidian
  • Database work → cy --mcp postgres
  • Quick code review → cy (no MCP overhead)
  • Complex integration → cy --mcp obsidian --mcp postgres (if needed, though the function doesn’t support multiple –mcp yet - that’s a TODO)

Breaking configs into single-purpose files turned out to be more useful than I expected.

Extending the Function

You can easily add more features:

Add Default MCP Config

1
2
3
4
5
# Add after local declarations
local default_mcp="obsidian"
if [[ -z "$mcp_config" && -f "$HOME/.claude/mcp/${default_mcp}.json" ]]; then
    mcp_config="--mcp-config $HOME/.claude/mcp/${default_mcp}.json"
fi

Add Verbose Mode

1
2
3
4
--verbose)
    claude_args="$claude_args --verbose"
    shift
    ;;

Add Config Validation

1
2
3
4
5
# Validate JSON before using
if ! jq empty "$mcp_file" 2>/dev/null; then
    echo "Error: Invalid JSON in $mcp_file"
    return 1
fi

Troubleshooting

Function Not Found: Make sure you sourced your shell configuration after adding the function

Permission Denied: Check that your MCP config files are readable

JSON Errors: Validate your MCP configuration files with jq

Path Issues: Verify ~/.claude/mcp/ directory exists and contains config files

Further Reading

If you’re setting up MCP servers, the Model Context Protocol spec explains how the protocol works. The Claude Code MCP documentation walks through config file format and available servers.

For more about context window optimization, Simon Willison wrote about managing LLM context budgets (if he hasn’t yet, he should - it’s a real concern as projects get larger).

The bash function documentation is surprisingly readable if you want to extend this pattern.

What I Learned

MCP servers are powerful but not free - they consume context window and add latency. Selective loading makes a real difference:

What I’ve noticed:

  • Sessions start noticeably faster without MCP overhead
  • I’m not hitting context limits on refactoring tasks like I used to
  • Most of my sessions don’t actually need MCP - code review, explanations, simple fixes work fine without it
  • When I do need MCP (working with Obsidian notes, database queries), I can enable just that specific capability

Time investment:

  • Building the function: couple hours one afternoon
  • Breaking monolithic config into separate files: maybe 30 minutes
  • Worth it if you use Claude Code regularly and hit context limits

The same pattern works for any CLI tool with expensive startup configuration. I’m using similar functions for docker (load different compose configs), pytest (different marker sets), and terraform (workspace selection).

If you’ve got a tool that has optional-but-costly configuration, selective loading via shell functions is worth trying.

uname -a: Human 5.4.0-anxiety #1 SMP Coffee-Deprived x86_64 GNU/Paranoid
Built with Hugo
Theme Stack designed by Jimmy