Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
bd9885f
Add regression test for stateless request memory cleanup (#1140)
felixweinberger Jul 15, 2025
eb5146d
Implement RFC9728 - Support WWW-Authenticate header by MCP client (#1…
yurikunash Jul 15, 2025
0130f5b
Add streamable HTTP starlette example to Python SDK docs (#1111)
pamelafox Jul 15, 2025
6566c08
fix markdown error in README in main (#1147)
ihrpr Jul 15, 2025
0b4ce00
README - replace code snippets with examples - add lowlevel to snippe…
ihrpr Jul 16, 2025
e975d05
README - replace code snippets with examples - streamable http (#1155)
ihrpr Jul 16, 2025
a99711d
chore: don't allow users to create issues outside the templates (#1163)
Kludex Jul 17, 2025
c0bc666
Tests(cli): Add coverage for helper functions (#635)
davenpi Jul 17, 2025
dd79297
Docs: Update CallToolResult parsing in README (#812)
functicons Jul 17, 2025
11162d7
docs: add pre-commit install guide on CONTRIBUTING.md (#995)
dingo4dev Jul 17, 2025
813da6a
fix flaky fix-test_streamablehttp_client_resumption test (#1166)
ihrpr Jul 17, 2025
41184ba
README - replace code snippets with examples -- auth examples (#1164)
ihrpr Jul 17, 2025
99c4f3c
Support falling back to OIDC metadata for auth (#1061)
LucaButBoring Jul 17, 2025
ba58ebc
Add CODEOWNERS file for sdk (#1169)
ihrpr Jul 18, 2025
0b1b52b
fix flaky test test_88_random_error (#1171)
ihrpr Jul 18, 2025
7570ba3
Make sure `RequestId` is not coerced as `int` (#1178)
Kludex Jul 21, 2025
c260e29
Fix: Replace threading.Lock with anyio.Lock for Ray deployment compat…
only-weng Jul 21, 2025
6a84a2f
fix: fix OAuth flow request object handling (#1174)
clareliguori Jul 21, 2025
6d2e6d4
update codeowners group (#1191)
ihrpr Jul 23, 2025
35777b9
fix: perform auth server metadata discovery fallbacks on any 4xx (#1193)
LucaButBoring Jul 24, 2025
b34e720
server: skip duplicate response on CancelledError (#1153)
lukacf Jul 25, 2025
959d4e3
Unpack settings in FastMCP (#1198)
Kludex Jul 26, 2025
6c61058
chore: Remove unused prompt_manager.py file (#1229)
chughtapan Aug 4, 2025
68e25d4
Improved supported for ProtectedResourceMetadata (#1235)
yannj-fr Aug 4, 2025
34e3664
chore: Remove unused variable notification_options (#1238)
sreenaths Aug 5, 2025
d475928
Improve README around the Context object (#1203)
jbkkd Aug 5, 2025
e68e513
fix: allow to pass `list[str]` to `token_endpoint_auth_signing_alg_va…
yannj-fr Aug 6, 2025
ef4e167
Remove strict validation on `response_modes_supported` member of `OAu…
joesavage-silabs Aug 7, 2025
c7671e4
Add pyright strict mode on the whole project (#1254)
Kludex Aug 11, 2025
a82c69b
Consistent casing for default headers Accept and Content-Type (#1263)
glinf Aug 12, 2025
0926613
Update dependencies and fix type issues (#1268)
dsp-ant Aug 14, 2025
d1ac8d6
fix: prevent async generator cleanup errors in StreamableHTTP transpo…
mous222 Aug 14, 2025
824ef8a
chore: uncomment .idea/ in .gitignore (#1287)
maxisbey Aug 20, 2025
ff02c59
docs: clarify streamable_http_path configuration when mounting server…
felixweinberger Aug 21, 2025
09e3a05
feat: Add CORS configuration for browser-based MCP clients (#1059)
jerome3o-anthropic Aug 21, 2025
f4b2957
Added Audio to FastMCP (#1130)
dragonier23 Aug 22, 2025
e750a06
fix: avoid uncessary retries in OAuth authenticated requests (#1206)
keurcien Aug 22, 2025
9a8592e
Add PATHEXT to default STDIO env vars in windows (#1256)
timesler Aug 22, 2025
eaf7cf4
fix: error too many values to unpack (expected 2) (#1279)
sandangel Aug 23, 2025
9c6fd15
SDK Parity: Avoid Parsing Server Response for non-JsonRPCMessage Requ…
justin-yi-wang Aug 26, 2025
07ae8c0
types: Setting default value for method: Literal (#1292)
sreenaths Aug 26, 2025
1644b82
changes structured temperature to not deadly (#1328)
monkeywithacupcake Aug 31, 2025
356dfa6
Update simple-resource example to use non-deprecated read_resource re…
pja-ant Sep 1, 2025
346e794
docs: Update README to include link to API docs for #1329 (#1330)
reidg44 Sep 1, 2025
47d35f0
Allow ping requests before initialization (#1312)
eleftherias Sep 1, 2025
c47c767
Python lint: Ruff rules for pylint and code complexity (#525)
cclauss Sep 5, 2025
c3717e7
Fix context injection for resources and prompts (#1336)
dsp-ant Sep 11, 2025
ca5cb4c
fix(fastmcp): propagate mimeType in resource template list (#1186)
pchoudhury22 Sep 17, 2025
b41b917
fix: allow elicitations accepted without content (#1285)
owengo Sep 17, 2025
ca34666
Use --frozen in pre-commit config (#1375)
pja-ant Sep 17, 2025
7e93a9f
Return HTTP 403 for invalid Origin headers (#1353)
pja-ant Sep 22, 2025
20596e5
Add test for ProtectedResourceMetadataParsing (#1236)
yannj-fr Sep 23, 2025
03e19f1
Fastmcp logging progress example (#1270)
stevebillings Sep 23, 2025
4fb975c
feat: add paginated list decorators for prompts, resources, and tools…
maxisbey Sep 23, 2025
7629fe6
Remove "unconditionally" from conditional description (#1289)
mssalvatore Sep 23, 2025
c0f1657
Use streamable-http consistently in examples (#1389)
maxisbey Sep 23, 2025
b85e7bd
feat: Add SDK support for SEP-1034 default values in elicitation sche…
chughtapan Sep 24, 2025
71889d7
Implementation of SEP 973 - Additional metadata + icons support (#1357)
pja-ant Sep 24, 2025
1bfcb94
Merge remote-tracking branch 'upstream/main' into sync-upstream-latest
dvlpjrs Sep 24, 2025
7c5136d
Merge upstream/main with custom filtering
dvlpjrs Sep 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattribute
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Generated
uv.lock linguist-generated=true
23 changes: 23 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# CODEOWNERS for MCP Python SDK
# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners

# Default maintainers for everything
* @modelcontextprotocol/python-sdk

# Auth-related code requires additional review from auth team
/src/mcp/client/auth.py @modelcontextprotocol/python-sdk-auth
/src/mcp/server/auth/ @modelcontextprotocol/python-sdk-auth
/src/mcp/server/transport_security.py @modelcontextprotocol/python-sdk-auth
/src/mcp/shared/auth*.py @modelcontextprotocol/python-sdk-auth

# Auth-related tests
/tests/client/test_auth.py @modelcontextprotocol/python-sdk-auth
/tests/server/auth/ @modelcontextprotocol/python-sdk-auth
/tests/server/test_*security.py @modelcontextprotocol/python-sdk-auth
/tests/server/fastmcp/auth/ @modelcontextprotocol/python-sdk-auth
/tests/shared/test_auth*.py @modelcontextprotocol/python-sdk-auth

# Auth-related examples
/examples/clients/simple-auth-client/ @modelcontextprotocol/python-sdk-auth
/examples/snippets/clients/oauth_client.py @modelcontextprotocol/python-sdk-auth
/examples/snippets/servers/oauth_server.py @modelcontextprotocol/python-sdk-auth
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yaml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
blank_issues_enabled: true
blank_issues_enabled: false
13 changes: 5 additions & 8 deletions .github/workflows/shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
permissions:
contents: read

env:
COLUMNS: 150

jobs:
pre-commit:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -33,6 +36,7 @@ jobs:
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
dep-resolution: ["lowest-direct", "highest"]
os: [ubuntu-latest, windows-latest]

steps:
Expand All @@ -45,18 +49,11 @@ jobs:
version: 0.7.2

- name: Install the project
run: uv sync --frozen --all-extras --python ${{ matrix.python-version }}
run: uv sync --frozen --all-extras --python ${{ matrix.python-version }} --resolution ${{ matrix.dep-resolution }}

- name: Run pytest
run: uv run --frozen --no-sync pytest

# This must run last as it modifies the environment!
- name: Run pytest with lowest versions
run: |
uv sync --all-extras --upgrade
uv run --no-sync pytest
env:
UV_RESOLUTION: lowest-direct
readme-snippets:
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
.ruff_cache/
cover/

# Translations
Expand Down Expand Up @@ -162,9 +163,12 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/

# vscode
.vscode/
.windsurfrules
**/CLAUDE.local.md

# claude code
.claude/
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ repos:
hooks:
- id: ruff-format
name: Ruff Format
entry: uv run ruff
entry: uv run --frozen ruff
args: [format]
language: system
types: [python]
pass_filenames: false
- id: ruff
name: Ruff
entry: uv run ruff
entry: uv run --frozen ruff
args: ["check", "--fix", "--exit-non-zero-on-fix"]
types: [python]
language: system
pass_filenames: false
exclude: ^README\.md$
- id: pyright
name: pyright
entry: uv run pyright
entry: uv run --frozen pyright
language: system
types: [python]
pass_filenames: false
Expand All @@ -52,7 +52,7 @@ repos:
pass_filenames: false
- id: readme-snippets
name: Check README snippets are up to date
entry: uv run scripts/update_readme_snippets.py --check
entry: uv run --frozen python scripts/update_readme_snippets.py --check
language: system
files: ^(README\.md|examples/.*\.py|scripts/update_readme_snippets\.py)$
pass_filenames: false
14 changes: 13 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Thank you for your interest in contributing to the MCP Python SDK! This document
uv sync --frozen --all-extras --dev
```

6. Set up pre-commit hooks:

```bash
uv tool install pre-commit --with pre-commit-uv --force-reinstall
```

## Development Workflow

1. Choose the correct branch for your changes:
Expand Down Expand Up @@ -50,7 +56,13 @@ uv run ruff format .
uv run scripts/update_readme_snippets.py
```

8. Submit a pull request to the same branch you branched from
8. (Optional) Run pre-commit hooks on all files:

```bash
pre-commit run --all-files
```

9. Submit a pull request to the same branch you branched from

## Code Style

Expand Down
2 changes: 1 addition & 1 deletion examples/clients/simple-auth-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@ mcp> quit
## Configuration

- `MCP_SERVER_PORT` - Server URL (default: 8000)
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable_http` (default) or `sse`
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable-http` (default) or `sse`
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def get_state(self):
class SimpleAuthClient:
"""Simple MCP client with auth support."""

def __init__(self, server_url: str, transport_type: str = "streamable_http"):
def __init__(self, server_url: str, transport_type: str = "streamable-http"):
self.server_url = server_url
self.transport_type = transport_type
self.session: ClientSession | None = None
Expand Down Expand Up @@ -188,9 +188,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
# Create OAuth authentication handler using the new interface
oauth_auth = OAuthClientProvider(
server_url=self.server_url.replace("/mcp", ""),
client_metadata=OAuthClientMetadata.model_validate(
client_metadata_dict
),
client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),
storage=InMemoryTokenStorage(),
redirect_handler=_default_redirect_handler,
callback_handler=callback_handler,
Expand Down Expand Up @@ -322,9 +320,7 @@ async def interactive_loop(self):
await self.call_tool(tool_name, arguments)

else:
print(
"❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'"
)
print("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'")

except KeyboardInterrupt:
print("\n\n👋 Goodbye!")
Expand All @@ -338,10 +334,10 @@ async def main():
# Default server URL - can be overridden with environment variable
# Most MCP streamable HTTP servers use /mcp as the endpoint
server_url = os.getenv("MCP_SERVER_PORT", 8000)
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable_http")
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable-http")
server_url = (
f"http://localhost:{server_url}/mcp"
if transport_type == "streamable_http"
if transport_type == "streamable-http"
else f"http://localhost:{server_url}/sse"
)

Expand Down
4 changes: 2 additions & 2 deletions examples/clients/simple-auth-client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
]
dependencies = [
"click>=8.0.0",
"click>=8.2.0",
"mcp>=1.0.0",
]

Expand All @@ -39,7 +39,7 @@ select = ["E", "F", "I"]
ignore = []

[tool.ruff]
line-length = 88
line-length = 120
target-version = "py310"

[tool.uv]
Expand Down
57 changes: 13 additions & 44 deletions examples/clients/simple-chatbot/mcp_simple_chatbot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
from mcp.client.stdio import stdio_client

# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")


class Configuration:
Expand Down Expand Up @@ -75,29 +73,19 @@ def __init__(self, name: str, config: dict[str, Any]) -> None:

async def initialize(self) -> None:
"""Initialize the server connection."""
command = (
shutil.which("npx")
if self.config["command"] == "npx"
else self.config["command"]
)
command = shutil.which("npx") if self.config["command"] == "npx" else self.config["command"]
if command is None:
raise ValueError("The command must be a valid string and cannot be None.")

server_params = StdioServerParameters(
command=command,
args=self.config["args"],
env={**os.environ, **self.config["env"]}
if self.config.get("env")
else None,
env={**os.environ, **self.config["env"]} if self.config.get("env") else None,
)
try:
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
read, write = stdio_transport
session = await self.exit_stack.enter_async_context(
ClientSession(read, write)
)
session = await self.exit_stack.enter_async_context(ClientSession(read, write))
await session.initialize()
self.session = session
except Exception as e:
Expand All @@ -122,10 +110,7 @@ async def list_tools(self) -> list[Any]:

for item in tools_response:
if isinstance(item, tuple) and item[0] == "tools":
tools.extend(
Tool(tool.name, tool.description, tool.inputSchema, tool.title)
for tool in item[1]
)
tools.extend(Tool(tool.name, tool.description, tool.inputSchema, tool.title) for tool in item[1])

return tools

Expand Down Expand Up @@ -164,9 +149,7 @@ async def execute_tool(

except Exception as e:
attempt += 1
logging.warning(
f"Error executing tool: {e}. Attempt {attempt} of {retries}."
)
logging.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.")
if attempt < retries:
logging.info(f"Retrying in {delay} seconds...")
await asyncio.sleep(delay)
Expand Down Expand Up @@ -209,9 +192,7 @@ def format_for_llm(self) -> str:
args_desc = []
if "properties" in self.input_schema:
for param_name, param_info in self.input_schema["properties"].items():
arg_desc = (
f"- {param_name}: {param_info.get('description', 'No description')}"
)
arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}"
if param_name in self.input_schema.get("required", []):
arg_desc += " (required)"
args_desc.append(arg_desc)
Expand Down Expand Up @@ -281,10 +262,7 @@ def get_response(self, messages: list[dict[str, str]]) -> str:
logging.error(f"Status code: {status_code}")
logging.error(f"Response details: {e.response.text}")

return (
f"I encountered an error: {error_message}. "
"Please try again or rephrase your request."
)
return f"I encountered an error: {error_message}. Please try again or rephrase your request."


class ChatSession:
Expand Down Expand Up @@ -323,17 +301,13 @@ async def process_llm_response(self, llm_response: str) -> str:
tools = await server.list_tools()
if any(tool.name == tool_call["tool"] for tool in tools):
try:
result = await server.execute_tool(
tool_call["tool"], tool_call["arguments"]
)
result = await server.execute_tool(tool_call["tool"], tool_call["arguments"])

if isinstance(result, dict) and "progress" in result:
progress = result["progress"]
total = result["total"]
percentage = (progress / total) * 100
logging.info(
f"Progress: {progress}/{total} ({percentage:.1f}%)"
)
logging.info(f"Progress: {progress}/{total} ({percentage:.1f}%)")

return f"Tool execution result: {result}"
except Exception as e:
Expand Down Expand Up @@ -408,9 +382,7 @@ async def start(self) -> None:

final_response = self.llm_client.get_response(messages)
logging.info("\nFinal response: %s", final_response)
messages.append(
{"role": "assistant", "content": final_response}
)
messages.append({"role": "assistant", "content": final_response})
else:
messages.append({"role": "assistant", "content": llm_response})

Expand All @@ -426,10 +398,7 @@ async def main() -> None:
"""Initialize and run the chat session."""
config = Configuration()
server_config = config.load_config("servers_config.json")
servers = [
Server(name, srv_config)
for name, srv_config in server_config["mcpServers"].items()
]
servers = [Server(name, srv_config) for name, srv_config in server_config["mcpServers"].items()]
llm_client = LLMClient(config.llm_api_key)
chat_session = ChatSession(servers, llm_client)
await chat_session.start()
Expand Down
2 changes: 1 addition & 1 deletion examples/clients/simple-chatbot/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ select = ["E", "F", "I"]
ignore = []

[tool.ruff]
line-length = 88
line-length = 120
target-version = "py310"

[tool.uv]
Expand Down
Loading
Loading