Skip to content

Implement support for client_credentials #1020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 85 additions & 0 deletions examples/clients/simple-auth-client-client-credentials/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Simple Auth Client Example

A demonstration of how to use the MCP Python SDK with OAuth authentication using client credentials over streamable HTTP or SSE transport.
This example demonstrates integration with an authorization server that does not implement Dynamic Client Registration.

## Features

- OAuth 2.0 authentication with the `client_credentials` flow
- Support for both StreamableHTTP and SSE transports
- Interactive command-line interface

## Installation

```bash
cd examples/clients/simple-auth-client-client-credentials
uv sync --reinstall
```

## Usage

### 1. Start an MCP server with OAuth support using client credentials

```bash
# Example with mcp-simple-auth-client-credentials
cd path/to/mcp-simple-auth-client-credentials
uv run mcp-simple-auth-client-credentials --transport streamable-http --port 3001
```

### 2. Run the client

```bash
uv run mcp-simple-auth-client

# Or with custom server URL
MCP_SERVER_PORT=3001 uv run mcp-simple-auth-client

# Use SSE transport
MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
```

### 3. Complete OAuth flow

The client will automatically authenticate using dummy client credentials for the demo authorization server. After completing OAuth, you can use commands:

- `list` - List available tools
- `call <tool_name> [args]` - Call a tool with optional JSON arguments
- `quit` - Exit

## Example

```
🚀 Simple MCP Auth Client
Connecting to: http://localhost:8001/mcp
Transport type: streamable_http
🔗 Attempting to connect to http://localhost:8001/mcp...
📡 Opening StreamableHTTP transport connection with auth...
🤝 Initializing MCP session...
⚡ Starting session initialization...
✨ Session initialization complete!

✅ Connected to MCP server at http://localhost:8001/mcp
Session ID: ...

🎯 Interactive MCP Client
Commands:
list - List available tools
call <tool_name> [args] - Call a tool
quit - Exit the client

mcp> list
📋 Available tools:
1. echo - Echo back the input text

mcp> call echo {"text": "Hello, world!"}
🔧 Tool 'echo' result:
Hello, world!

mcp> quit
👋 Goodbye!
```

## Configuration

- `MCP_SERVER_PORT` - Server URL (default: 8000)
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable_http` (default) or `sse`
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Simple OAuth client for MCP simple-auth server."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""
Simple MCP client example with OAuth authentication support.

This client connects to an MCP server using streamable HTTP transport with OAuth.

"""

import asyncio
import os
from datetime import timedelta
from typing import Any

from mcp.client.auth import OAuthClientProvider, TokenStorage
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken

# Hardcoded credentials assuming a preconfigured client, to demonstrate
# working with an AS that does not have DCR support
MCP_CLIENT_ID = "0000000000000000000"
MCP_CLIENT_SECRET = "aaaaaaaaaaaaaaaaaaa"


class InMemoryTokenStorage(TokenStorage):
"""Simple in-memory token storage implementation."""

def __init__(self, client_id: str | None, client_secret: str | None):
self._tokens: OAuthToken | None = None
self._client_info = OAuthClientInformationFull(
client_id=client_id,
client_secret=client_secret,
redirect_uris=None,
)

async def get_tokens(self) -> OAuthToken | None:
return self._tokens

async def set_tokens(self, tokens: OAuthToken) -> None:
self._tokens = tokens

async def get_client_info(self) -> OAuthClientInformationFull | None:
return self._client_info

async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self._client_info = client_info


class SimpleAuthClient:
"""Simple MCP client with auth support."""

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

async def connect(self):
"""Connect to the MCP server."""
print(f"🔗 Attempting to connect to {self.server_url}...")

try:
client_metadata_dict = {
"client_name": "Simple Auth Client",
"redirect_uris": None,
"grant_types": ["client_credentials"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "identify",
}

# 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),
storage=InMemoryTokenStorage(
client_id=MCP_CLIENT_ID,
client_secret=MCP_CLIENT_SECRET,
),
)
oauth_auth.context.client_info = OAuthClientInformationFull(
redirect_uris=None,
)

# Create transport with auth handler based on transport type
if self.transport_type == "sse":
print("📡 Opening SSE transport connection with auth...")
async with sse_client(
url=self.server_url,
auth=oauth_auth,
timeout=60,
) as (read_stream, write_stream):
await self._run_session(read_stream, write_stream, None)
else:
print("📡 Opening StreamableHTTP transport connection with auth...")
async with streamablehttp_client(
url=self.server_url,
auth=oauth_auth,
timeout=timedelta(seconds=60),
) as (read_stream, write_stream, get_session_id):
await self._run_session(read_stream, write_stream, get_session_id)

except Exception as e:
print(f"❌ Failed to connect: {e}")
import traceback

traceback.print_exc()

async def _run_session(self, read_stream, write_stream, get_session_id):
"""Run the MCP session with the given streams."""
print("🤝 Initializing MCP session...")
async with ClientSession(read_stream, write_stream) as session:
self.session = session
print("⚡ Starting session initialization...")
await session.initialize()
print("✨ Session initialization complete!")

print(f"\n✅ Connected to MCP server at {self.server_url}")
if get_session_id:
session_id = get_session_id()
if session_id:
print(f"Session ID: {session_id}")

# Run interactive loop
await self.interactive_loop()

async def list_tools(self):
"""List available tools from the server."""
if not self.session:
print("❌ Not connected to server")
return

try:
result = await self.session.list_tools()
if hasattr(result, "tools") and result.tools:
print("\n📋 Available tools:")
for i, tool in enumerate(result.tools, 1):
print(f"{i}. {tool.name}")
if tool.description:
print(f" Description: {tool.description}")
print()
else:
print("No tools available")
except Exception as e:
print(f"❌ Failed to list tools: {e}")

async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None):
"""Call a specific tool."""
if not self.session:
print("❌ Not connected to server")
return

try:
result = await self.session.call_tool(tool_name, arguments or {})
print(f"\n🔧 Tool '{tool_name}' result:")
if hasattr(result, "content"):
for content in result.content:
if content.type == "text":
print(content.text)
else:
print(content)
else:
print(result)
except Exception as e:
print(f"❌ Failed to call tool '{tool_name}': {e}")

async def interactive_loop(self):
"""Run interactive command loop."""
print("\n🎯 Interactive MCP Client")
print("Commands:")
print(" list - List available tools")
print(" call <tool_name> [args] - Call a tool")
print(" quit - Exit the client")
print()

while True:
try:
command = input("mcp> ").strip()

if not command:
continue

if command == "quit":
break

elif command == "list":
await self.list_tools()

elif command.startswith("call "):
parts = command.split(maxsplit=2)
tool_name = parts[1] if len(parts) > 1 else ""

if not tool_name:
print("❌ Please specify a tool name")
continue

# Parse arguments (simple JSON-like format)
arguments = {}
if len(parts) > 2:
import json

try:
arguments = json.loads(parts[2])
except json.JSONDecodeError:
print("❌ Invalid arguments format (expected JSON)")
continue

await self.call_tool(tool_name, arguments)

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

except KeyboardInterrupt:
print("\n\n👋 Goodbye!")
break
except EOFError:
break


async def main():
"""Main entry point."""
# 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")
server_url = (
f"http://localhost:{server_url}/mcp"
if transport_type == "streamable_http"
else f"http://localhost:{server_url}/sse"
)

print("🚀 Simple MCP Auth Client")
print(f"Connecting to: {server_url}")
print(f"Transport type: {transport_type}")

# Start connection flow - OAuth will be handled automatically
client = SimpleAuthClient(server_url, transport_type)
await client.connect()


def cli():
"""CLI entry point for uv script."""
asyncio.run(main())


if __name__ == "__main__":
cli()
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[project]
name = "mcp-simple-auth-client-client-credentials"
version = "0.1.0"
description = "A simple OAuth client for the MCP simple-auth server"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Anthropic" }]
keywords = ["mcp", "oauth", "client", "auth"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = ["click>=8.0.0", "mcp"]

[project.scripts]
mcp-simple-auth-client-client-credentials = "mcp_simple_auth_client_client_credentials.main:cli"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_auth_client_client_credentials"]

[tool.uv]
dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]
11 changes: 1 addition & 10 deletions examples/clients/simple-auth-client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = [
"click>=8.0.0",
"mcp>=1.0.0",
]
dependencies = ["click>=8.0.0", "mcp"]

[project.scripts]
mcp-simple-auth-client = "mcp_simple_auth_client.main:cli"
Expand All @@ -44,9 +41,3 @@ target-version = "py310"

[tool.uv]
dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]

[tool.uv.sources]
mcp = { path = "../../../" }

[[tool.uv.index]]
url = "https://pypi.org/simple"
Loading
Loading