From 783c0f4d332618e4f15749b136703d8ae0a3ba8e Mon Sep 17 00:00:00 2001 From: Lina Tawfik Date: Thu, 3 Jul 2025 14:51:04 -0700 Subject: [PATCH 1/5] feat: add strict_mcp_config option to Python SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add strict_mcp_config boolean field to ClaudeCodeOptions - Pass --strict-mcp-config CLI flag when option is True - Add tests for the new option - Add example demonstrating usage - Update README with MCP server configuration docs This enables SDK users to ignore all file-based MCP configurations and use only programmatically specified servers. Fixes #45 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 26 ++++++++++++ examples/strict_mcp_config_example.py | 41 +++++++++++++++++++ .../_internal/transport/subprocess_cli.py | 3 ++ src/claude_code_sdk/types.py | 1 + tests/test_transport.py | 21 ++++++++++ tests/test_types.py | 9 ++++ 6 files changed, 101 insertions(+) create mode 100644 examples/strict_mcp_config_example.py diff --git a/README.md b/README.md index fd91924..9141fbc 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,32 @@ options = ClaudeCodeOptions( ) ``` +### MCP Servers + +```python +# Configure MCP servers programmatically +options = ClaudeCodeOptions( + mcp_servers={ + "memory-server": { + "command": "npx", + "args": ["@modelcontextprotocol/server-memory"] + } + } +) + +# Use strict MCP config to ignore all file-based configurations +# This ensures ONLY your programmatically specified servers are used +options = ClaudeCodeOptions( + mcp_servers={ + "my-server": { + "command": "node", + "args": ["my-mcp-server.js"] + } + }, + strict_mcp_config=True # Ignore global/project MCP settings +) +``` + ## API Reference ### `query(prompt, options=None)` diff --git a/examples/strict_mcp_config_example.py b/examples/strict_mcp_config_example.py new file mode 100644 index 0000000..e604353 --- /dev/null +++ b/examples/strict_mcp_config_example.py @@ -0,0 +1,41 @@ +"""Example demonstrating how to use strict MCP config with Claude SDK. + +This example shows how to use the strict_mcp_config option to ensure +only your programmatically specified MCP servers are used, ignoring +any global or project-level MCP configurations. +""" + +from claude_code_sdk import ClaudeCodeSDK, ClaudeCodeOptions + +async def main(): + # Create options with strict MCP config enabled + # This ensures ONLY the MCP servers specified here will be used + options = ClaudeCodeOptions( + mcp_servers={ + "my-custom-server": { + "command": "npx", + "args": ["@modelcontextprotocol/server-memory"], + } + }, + strict_mcp_config=True, # Ignore all file-based MCP configurations + ) + + # Create SDK instance + sdk = ClaudeCodeSDK() + + # Query Claude with strict MCP config + async with await sdk.query( + "List the available MCP tools from the memory server", + options=options + ) as session: + async for message in session.stream(): + if message.type == "assistant": + print(f"Claude: {message.message.content}") + elif message.type == "result": + print(f"\nResult: {message.subtype}") + if message.result: + print(f"Final output: {message.result}") + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) \ No newline at end of file diff --git a/src/claude_code_sdk/_internal/transport/subprocess_cli.py b/src/claude_code_sdk/_internal/transport/subprocess_cli.py index f4fbc58..2915ac8 100644 --- a/src/claude_code_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_code_sdk/_internal/transport/subprocess_cli.py @@ -116,6 +116,9 @@ class SubprocessCLITransport(Transport): ["--mcp-config", json.dumps({"mcpServers": self._options.mcp_servers})] ) + if self._options.strict_mcp_config: + cmd.append("--strict-mcp-config") + cmd.extend(["--print", self._prompt]) return cmd diff --git a/src/claude_code_sdk/types.py b/src/claude_code_sdk/types.py index bd3c726..5d2ce95 100644 --- a/src/claude_code_sdk/types.py +++ b/src/claude_code_sdk/types.py @@ -127,3 +127,4 @@ class ClaudeCodeOptions: model: str | None = None permission_prompt_tool_name: str | None = None cwd: str | Path | None = None + strict_mcp_config: bool = False diff --git a/tests/test_transport.py b/tests/test_transport.py index 65702bc..c2322d8 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -132,3 +132,24 @@ class TestSubprocessCLITransport: # So we just verify the transport can be created and basic structure is correct assert transport._prompt == "test" assert transport._cli_path == "/usr/bin/claude" + + def test_build_command_with_strict_mcp_config(self): + """Test building CLI command with strict MCP config.""" + transport = SubprocessCLITransport( + prompt="test", + options=ClaudeCodeOptions(strict_mcp_config=True), + cli_path="/usr/bin/claude", + ) + + cmd = transport._build_command() + assert "--strict-mcp-config" in cmd + + # Test that flag is not present when False + transport_no_strict = SubprocessCLITransport( + prompt="test", + options=ClaudeCodeOptions(strict_mcp_config=False), + cli_path="/usr/bin/claude", + ) + + cmd_no_strict = transport_no_strict._build_command() + assert "--strict-mcp-config" not in cmd_no_strict diff --git a/tests/test_types.py b/tests/test_types.py index 6046292..794266c 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -105,3 +105,12 @@ class TestOptions: ) assert options.model == "claude-3-5-sonnet-20241022" assert options.permission_prompt_tool_name == "CustomTool" + + def test_claude_code_options_with_strict_mcp_config(self): + """Test Options with strict MCP config.""" + options = ClaudeCodeOptions(strict_mcp_config=True) + assert options.strict_mcp_config is True + + # Test default value + default_options = ClaudeCodeOptions() + assert default_options.strict_mcp_config is False From 6a6991f48c60efca9fec0f67bf0f114a7e5f45fd Mon Sep 17 00:00:00 2001 From: Lina Tawfik Date: Thu, 3 Jul 2025 14:56:56 -0700 Subject: [PATCH 2/5] fix: resolve linting issues and simplify tests - Remove trailing whitespace from test files - Consolidate strict_mcp_config test into test_default_options - Keep only the essential transport test for CLI flag verification --- tests/test_transport.py | 4 ++-- tests/test_types.py | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index c2322d8..e8898e4 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -143,13 +143,13 @@ class TestSubprocessCLITransport: cmd = transport._build_command() assert "--strict-mcp-config" in cmd - + # Test that flag is not present when False transport_no_strict = SubprocessCLITransport( prompt="test", options=ClaudeCodeOptions(strict_mcp_config=False), cli_path="/usr/bin/claude", ) - + cmd_no_strict = transport_no_strict._build_command() assert "--strict-mcp-config" not in cmd_no_strict diff --git a/tests/test_types.py b/tests/test_types.py index 794266c..2d6b268 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -69,6 +69,7 @@ class TestOptions: assert options.permission_mode is None assert options.continue_conversation is False assert options.disallowed_tools == [] + assert options.strict_mcp_config is False def test_claude_code_options_with_tools(self): """Test Options with built-in tools.""" @@ -105,12 +106,3 @@ class TestOptions: ) assert options.model == "claude-3-5-sonnet-20241022" assert options.permission_prompt_tool_name == "CustomTool" - - def test_claude_code_options_with_strict_mcp_config(self): - """Test Options with strict MCP config.""" - options = ClaudeCodeOptions(strict_mcp_config=True) - assert options.strict_mcp_config is True - - # Test default value - default_options = ClaudeCodeOptions() - assert default_options.strict_mcp_config is False From b73eac8c57a7eb7605892b21892eb479026f852d Mon Sep 17 00:00:00 2001 From: Lina Tawfik Date: Thu, 3 Jul 2025 16:16:57 -0700 Subject: [PATCH 3/5] refactor: consolidate strict_mcp_config example into quick_start.py - Remove separate strict_mcp_config_example.py file - Add with_strict_mcp_config_example() to quick_start.py - Use simpler query() function for consistency - Add comment to main() about uncommenting MCP example if servers configured --- examples/quick_start.py | 31 ++++++++++++++++++++ examples/strict_mcp_config_example.py | 41 --------------------------- 2 files changed, 31 insertions(+), 41 deletions(-) delete mode 100644 examples/strict_mcp_config_example.py diff --git a/examples/quick_start.py b/examples/quick_start.py index 37d93b0..5387a56 100644 --- a/examples/quick_start.py +++ b/examples/quick_start.py @@ -65,11 +65,42 @@ async def with_tools_example(): print() +async def with_strict_mcp_config_example(): + """Example using strict MCP configuration.""" + print("=== Strict MCP Config Example ===") + + # This ensures ONLY the MCP servers specified here will be used, + # ignoring any global or project-level MCP configurations + options = ClaudeCodeOptions( + mcp_servers={ + "memory-server": { + "command": "npx", + "args": ["@modelcontextprotocol/server-memory"], + } + }, + strict_mcp_config=True, # Ignore all file-based MCP configurations + ) + + async for message in query( + prompt="List the available MCP tools from the memory server", + options=options, + ): + if isinstance(message, AssistantMessage): + for block in message.content: + if isinstance(block, TextBlock): + print(f"Claude: {block.text}") + elif isinstance(message, ResultMessage): + print(f"\nResult: {message.subtype}") + print() + + async def main(): """Run all examples.""" await basic_example() await with_options_example() await with_tools_example() + # Note: Uncomment the line below if you have MCP servers configured + # await with_strict_mcp_config_example() if __name__ == "__main__": diff --git a/examples/strict_mcp_config_example.py b/examples/strict_mcp_config_example.py deleted file mode 100644 index e604353..0000000 --- a/examples/strict_mcp_config_example.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Example demonstrating how to use strict MCP config with Claude SDK. - -This example shows how to use the strict_mcp_config option to ensure -only your programmatically specified MCP servers are used, ignoring -any global or project-level MCP configurations. -""" - -from claude_code_sdk import ClaudeCodeSDK, ClaudeCodeOptions - -async def main(): - # Create options with strict MCP config enabled - # This ensures ONLY the MCP servers specified here will be used - options = ClaudeCodeOptions( - mcp_servers={ - "my-custom-server": { - "command": "npx", - "args": ["@modelcontextprotocol/server-memory"], - } - }, - strict_mcp_config=True, # Ignore all file-based MCP configurations - ) - - # Create SDK instance - sdk = ClaudeCodeSDK() - - # Query Claude with strict MCP config - async with await sdk.query( - "List the available MCP tools from the memory server", - options=options - ) as session: - async for message in session.stream(): - if message.type == "assistant": - print(f"Claude: {message.message.content}") - elif message.type == "result": - print(f"\nResult: {message.subtype}") - if message.result: - print(f"Final output: {message.result}") - -if __name__ == "__main__": - import asyncio - asyncio.run(main()) \ No newline at end of file From b123767bc1c6ee345050d0f9585d1ba2b7de2e21 Mon Sep 17 00:00:00 2001 From: Lina Tawfik Date: Thu, 3 Jul 2025 16:18:00 -0700 Subject: [PATCH 4/5] fix: remove trailing whitespace in examples/quick_start.py --- examples/quick_start.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/quick_start.py b/examples/quick_start.py index 5387a56..a6dd715 100644 --- a/examples/quick_start.py +++ b/examples/quick_start.py @@ -68,7 +68,7 @@ async def with_tools_example(): async def with_strict_mcp_config_example(): """Example using strict MCP configuration.""" print("=== Strict MCP Config Example ===") - + # This ensures ONLY the MCP servers specified here will be used, # ignoring any global or project-level MCP configurations options = ClaudeCodeOptions( @@ -80,7 +80,7 @@ async def with_strict_mcp_config_example(): }, strict_mcp_config=True, # Ignore all file-based MCP configurations ) - + async for message in query( prompt="List the available MCP tools from the memory server", options=options, From affe3c9d92426c992b076482b3e39087be3ce70d Mon Sep 17 00:00:00 2001 From: Lina Tawfik Date: Thu, 3 Jul 2025 16:57:11 -0700 Subject: [PATCH 5/5] refactor: reorganize examples for better clarity - Simplify quick_start.py to just basic examples (~45 lines) - Move tools example to dedicated using_tools.py file - Move MCP configuration example to mcp_servers.py file - No new content added - just reorganized existing examples This makes the quick start truly quick and provides dedicated files for users who need specific advanced features. --- examples/mcp_servers.py | 50 ++++++++++++++++++++++++++++++++++++++ examples/quick_start.py | 54 ----------------------------------------- examples/using_tools.py | 43 ++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 54 deletions(-) create mode 100644 examples/mcp_servers.py create mode 100644 examples/using_tools.py diff --git a/examples/mcp_servers.py b/examples/mcp_servers.py new file mode 100644 index 0000000..dbf3121 --- /dev/null +++ b/examples/mcp_servers.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Example demonstrating MCP (Model Context Protocol) server configuration.""" + +import anyio + +from claude_code_sdk import ( + AssistantMessage, + ClaudeCodeOptions, + ResultMessage, + TextBlock, + query, +) + + +async def with_strict_mcp_config_example(): + """Example using strict MCP configuration.""" + print("=== Strict MCP Config Example ===") + + # This ensures ONLY the MCP servers specified here will be used, + # ignoring any global or project-level MCP configurations + options = ClaudeCodeOptions( + mcp_servers={ + "memory-server": { + "command": "npx", + "args": ["@modelcontextprotocol/server-memory"], + } + }, + strict_mcp_config=True, # Ignore all file-based MCP configurations + ) + + async for message in query( + prompt="List the available MCP tools from the memory server", + options=options, + ): + if isinstance(message, AssistantMessage): + for block in message.content: + if isinstance(block, TextBlock): + print(f"Claude: {block.text}") + elif isinstance(message, ResultMessage): + print(f"\nResult: {message.subtype}") + print() + + +async def main(): + """Run the example.""" + await with_strict_mcp_config_example() + + +if __name__ == "__main__": + anyio.run(main) \ No newline at end of file diff --git a/examples/quick_start.py b/examples/quick_start.py index a6dd715..cc1f2cc 100644 --- a/examples/quick_start.py +++ b/examples/quick_start.py @@ -43,64 +43,10 @@ async def with_options_example(): print() -async def with_tools_example(): - """Example using tools.""" - print("=== With Tools Example ===") - - options = ClaudeCodeOptions( - allowed_tools=["Read", "Write"], - system_prompt="You are a helpful file assistant.", - ) - - async for message in query( - prompt="Create a file called hello.txt with 'Hello, World!' in it", - options=options, - ): - if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") - elif isinstance(message, ResultMessage) and message.total_cost_usd > 0: - print(f"\nCost: ${message.total_cost_usd:.4f}") - print() - - -async def with_strict_mcp_config_example(): - """Example using strict MCP configuration.""" - print("=== Strict MCP Config Example ===") - - # This ensures ONLY the MCP servers specified here will be used, - # ignoring any global or project-level MCP configurations - options = ClaudeCodeOptions( - mcp_servers={ - "memory-server": { - "command": "npx", - "args": ["@modelcontextprotocol/server-memory"], - } - }, - strict_mcp_config=True, # Ignore all file-based MCP configurations - ) - - async for message in query( - prompt="List the available MCP tools from the memory server", - options=options, - ): - if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") - elif isinstance(message, ResultMessage): - print(f"\nResult: {message.subtype}") - print() - - async def main(): """Run all examples.""" await basic_example() await with_options_example() - await with_tools_example() - # Note: Uncomment the line below if you have MCP servers configured - # await with_strict_mcp_config_example() if __name__ == "__main__": diff --git a/examples/using_tools.py b/examples/using_tools.py new file mode 100644 index 0000000..eef0c0a --- /dev/null +++ b/examples/using_tools.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Example demonstrating how to use tools with Claude Code SDK.""" + +import anyio + +from claude_code_sdk import ( + AssistantMessage, + ClaudeCodeOptions, + ResultMessage, + TextBlock, + query, +) + + +async def with_tools_example(): + """Example using tools.""" + print("=== With Tools Example ===") + + options = ClaudeCodeOptions( + allowed_tools=["Read", "Write"], + system_prompt="You are a helpful file assistant.", + ) + + async for message in query( + prompt="Create a file called hello.txt with 'Hello, World!' in it", + options=options, + ): + if isinstance(message, AssistantMessage): + for block in message.content: + if isinstance(block, TextBlock): + print(f"Claude: {block.text}") + elif isinstance(message, ResultMessage) and message.total_cost_usd > 0: + print(f"\nCost: ${message.total_cost_usd:.4f}") + print() + + +async def main(): + """Run the example.""" + await with_tools_example() + + +if __name__ == "__main__": + anyio.run(main) \ No newline at end of file