Skip to content

Commit e9445d6

Browse files
authored
Fix tools for sse (#7)
* auth: anaconda * fix: tests * example: sse * fix: tools * bump: version
1 parent 7699d5c commit e9445d6

File tree

15 files changed

+1002
-112
lines changed

15 files changed

+1002
-112
lines changed

examples/proxy-anaconda/Makefile

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ help:
88
@echo " install - Install mcp-compose and dependencies"
99
@echo " setup-auth - Check Anaconda authentication setup"
1010
@echo " install-agent - Install pydantic-ai for agent support"
11+
@echo " start-echo-sse - Start the echo MCP server in SSE mode (port 8081)"
1112
@echo " start - Start the MCP Compose with authentication"
1213
@echo " agent - Run the AI agent (requires composer running)"
1314
@echo " stop - Stop the MCP Compose"
@@ -72,15 +73,32 @@ install-agent:
7273
@echo " 1. Start composer: make start"
7374
@echo " 2. In another terminal: make agent"
7475

76+
# Start the echo MCP server in SSE mode
77+
start-echo-sse:
78+
@echo "Starting Echo MCP Server in SSE mode..."
79+
@echo ""
80+
@echo "Configuration:"
81+
@echo " • Transport: SSE"
82+
@echo " • Port: 8081"
83+
@echo " • Endpoint: http://localhost:8081/sse"
84+
@echo ""
85+
@echo "Note: Keep this running in a separate terminal"
86+
@echo " Then run 'make start' to start the composer"
87+
@echo ""
88+
python mcp2_sse.py
89+
7590
# Start the MCP Compose (no auth key required to start)
7691
start:
7792
@echo "Starting MCP Compose..."
7893
@echo ""
7994
@echo "Configuration:"
80-
@echo " • Authentication: Bearer token validation (when implemented)"
81-
@echo " • Backend servers: calculator, echo"
95+
@echo " • Authentication: Anaconda bearer token validation"
96+
@echo " • Backend servers: calculator (STDIO), echo (SSE)"
8297
@echo " • Port: 8080"
8398
@echo ""
99+
@echo "Prerequisites:"
100+
@echo " 1. Echo SSE server must be running: make start-echo-sse"
101+
@echo ""
84102
@echo "Note: Server starts without requiring ANACONDA_API_KEY."
85103
@echo " Clients must provide valid Anaconda bearer tokens."
86104
@echo ""

examples/proxy-anaconda/README.md

Lines changed: 72 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ This example demonstrates how to use MCP Compose with **Anaconda authentication
1717
This configuration launches two simple Python MCP servers behind an authenticated MCP Compose:
1818

1919
1. **Calculator Server** (`mcp1.py`) - Math operations (add, subtract, multiply, divide)
20-
2. **Echo Server** (`mcp2.py`) - String operations (ping, echo, reverse, uppercase, lowercase, count_words)
20+
- Transport: **STDIO** (managed by composer)
21+
2. **Echo Server** (`mcp2_sse.py`) - String operations (ping, echo, reverse, uppercase, lowercase, count_words)
22+
- Transport: **SSE** (standalone HTTP server on port 8081)
2123

2224
Both servers:
23-
- Run in **proxy mode** via STDIO transport
24-
- Are managed by the MCP Compose
25+
- Run in **proxy mode** (STDIO and SSE transports)
26+
- Are managed/proxied by the MCP Compose
2527
- Are accessed through the **composer's authentication layer**
2628

2729
## 🔐 Authentication Architecture
@@ -92,19 +94,53 @@ This will install:
9294
- `fastmcp` (for the demo MCP servers)
9395
- `anaconda-auth` (for token validation)
9496

95-
### 2. Start the Composer
97+
### 2. Start the Echo SSE Server
98+
99+
First, start the echo server in SSE mode (in a separate terminal):
100+
101+
```bash
102+
make start-echo-sse
103+
```
104+
105+
This will start the echo MCP server on port 8081 with SSE transport.
106+
107+
### 3. Start the Composer
108+
109+
In another terminal:
96110

97111
```bash
98112
make start
99113
```
100114

101-
**No authentication required to start!** The composer will:
115+
**No Anaconda credentials required to start the server!** The composer will:
102116
- Read configuration from `mcp_compose.toml`
103-
- Start both Calculator and Echo MCP servers as child processes
117+
- Start the Calculator MCP server as a child process (STDIO)
118+
- Connect to the Echo MCP server via SSE (http://localhost:8081/sse)
104119
- Listen on port 8080 for client connections
105-
- Validate bearer tokens from incoming client requests
120+
- Validate bearer tokens from incoming client requests using `anaconda-auth`
121+
122+
You'll see output like:
123+
```
124+
🔐 Authentication enabled
125+
Provider: anaconda
126+
Domain: anaconda.com
127+
✓ Authenticator initialized
128+
129+
🚀 MCP Compose: anaconda-composer
130+
Starting 1 server(s)...
106131
107-
### 3. Connect with a Client
132+
• calculator
133+
Command: python mcp1.py
134+
Status: ✓ Started
135+
136+
Connecting to 1 SSE server(s)...
137+
138+
• echo
139+
URL: http://localhost:8081/sse
140+
Status: ✓ Connected
141+
```
142+
143+
### 4. Connect with a Client
108144

109145
Clients must provide an Anaconda bearer token in requests:
110146

@@ -129,7 +165,7 @@ mcp_server = MCPServerSSE(
129165
4. If valid, request is proxied to backend servers
130166
5. If invalid, request is rejected with 401 Unauthorized
131167

132-
### 4. Use the AI Agent (Coming Soon)
168+
### 5. Use the AI Agent (Coming Soon)
133169

134170
> **🚧 Work in Progress**: The agent integration requires the unified SSE endpoint to be implemented in the serve command. The agent.py file is ready and demonstrates the intended usage pattern.
135171
@@ -153,9 +189,11 @@ Example interactions (once SSE endpoint is available):
153189
- "Convert 'Hello World' to uppercase"
154190
- "Count the words in 'The quick brown fox jumps'"
155191

156-
### 5. Stop the Composer
192+
### 6. Stop the Servers
157193

158-
Press `Ctrl+C` in the terminal where the composer is running.
194+
Press `Ctrl+C` in both terminals:
195+
1. The terminal running the composer
196+
2. The terminal running the echo SSE server
159197

160198
## 🔧 How Authentication Works
161199

@@ -266,8 +304,7 @@ providers = ["anaconda"]
266304
default_provider = "anaconda"
267305

268306
[authentication.anaconda]
269-
domain = "anaconda.com"
270-
# API key loaded from ANACONDA_API_KEY environment variable
307+
domain = "anaconda.com" # Use "your-company.anaconda.com" for enterprise
271308

272309
# ============================================================================
273310
# Backend MCP Servers (No auth - accessed via composer only)
@@ -426,70 +463,39 @@ This example demonstrates Anaconda auth for the MCP servers themselves. For prod
426463

427464
See the [mcp-auth example](../mcp-auth/) for OAuth2 authentication at the composer level.
428465

429-
## 🚧 Implementation Status
466+
## Implementation Status
430467

431468
### What's Working
432469
- ✅ Backend MCP servers (calculator, echo)
433470
- ✅ MCP Compose configuration structure
434471
- ✅ Authentication framework in mcp-compose
472+
-**Anaconda Authenticator**: Implemented in `mcp_compose/auth_anaconda.py`
473+
-**Token validation**: Integration with `anaconda_auth.token.TokenInfo`
474+
-**Configuration loading**: Parse `[authentication.anaconda]` from TOML
475+
-**Middleware integration**: Applied to SSE/HTTP endpoints via FastAPI dependencies
435476

436-
### What Needs Implementation
437-
-**Anaconda Authenticator**: Need to implement `AnacondaAuthenticator` class
438-
-**Token validation**: Integration with `anaconda_auth.token.TokenInfo`
439-
-**Configuration loading**: Parse `[authentication.anaconda]` from TOML
440-
-**Middleware integration**: Apply auth to SSE/HTTP endpoints
477+
### Implementation Details
441478

442-
### Implementation Guide
479+
The Anaconda authentication has been fully implemented:
443480

444-
To implement Anaconda authentication, add to `mcp_compose/auth.py`:
481+
1. **AnacondaAuthenticator Class** (`mcp_compose/auth_anaconda.py`):
482+
- Validates bearer tokens using `anaconda_auth.token.TokenInfo`
483+
- Supports custom domains for enterprise deployments
484+
- Extracts user information from tokens
445485

446-
```python
447-
from anaconda_auth.token import TokenInfo
486+
2. **Configuration Support** (`mcp_compose/config.py`):
487+
- Added `AuthProvider.ANACONDA` enum value
488+
- Added `AnacondaAuthConfig` model with domain configuration
489+
- Validates Anaconda auth configuration when enabled
448490

449-
class AnacondaAuthenticator(Authenticator):
450-
"""Anaconda authentication using anaconda-auth library."""
451-
452-
def __init__(self, domain: str = "anaconda.com"):
453-
super().__init__(AuthType.API_KEY) # Or create AuthType.ANACONDA
454-
self.domain = domain
455-
456-
async def authenticate(self, credentials: Dict[str, Any]) -> AuthContext:
457-
"""Validate Anaconda token."""
458-
token = credentials.get("api_key") or credentials.get("token")
459-
if not token:
460-
raise InvalidCredentialsError("Anaconda token not provided")
461-
462-
try:
463-
# Validate with anaconda-auth
464-
token_info = TokenInfo(domain=self.domain, api_key=token)
465-
access_token = token_info.get_access_token()
466-
467-
if not access_token:
468-
raise InvalidCredentialsError("Invalid Anaconda token")
469-
470-
# Get user info from token
471-
user_id = self._get_user_from_token(access_token)
472-
473-
return AuthContext(
474-
user_id=user_id,
475-
auth_type=self.auth_type,
476-
token=token,
477-
scopes=["*"], # Or parse from token
478-
)
479-
except Exception as e:
480-
raise InvalidCredentialsError(f"Anaconda authentication failed: {e}")
481-
482-
async def validate(self, context: AuthContext) -> bool:
483-
"""Validate existing Anaconda token."""
484-
# Re-authenticate to check if token is still valid
485-
try:
486-
await self.authenticate({"token": context.token})
487-
return True
488-
except:
489-
return False
490-
```
491+
3. **API Integration** (`mcp_compose/api/dependencies.py`):
492+
- Bearer token extraction from Authorization header
493+
- Automatic authentication using configured authenticator
494+
- Anonymous access when authentication is disabled
491495

492-
See the [mcp-auth example](../mcp-auth/) for a complete OAuth2 implementation.
496+
4. **CLI Integration** (`mcp_compose/cli.py`):
497+
- Initializes authenticator from configuration
498+
- Displays authentication status on startup
493499

494500
## 📚 Learn More
495501

examples/proxy-anaconda/agent.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def get_anaconda_token() -> str:
7979
sys.exit(1)
8080

8181

82-
def create_agent(model: str = "anthropic:claude-sonnet-4-0", server_url: str = "http://localhost:8080") -> Agent:
82+
def create_agent(model: str = "anthropic:claude-sonnet-4-0", server_url: str = "http://localhost:8080") -> tuple[Agent, str]:
8383
"""
8484
Create a pydantic-ai Agent connected to the MCP Compose
8585
@@ -89,7 +89,7 @@ def create_agent(model: str = "anthropic:claude-sonnet-4-0", server_url: str = "
8989
server_url: MCP Compose base URL
9090
9191
Returns:
92-
Configured pydantic-ai Agent
92+
Tuple of (configured pydantic-ai Agent, access token)
9393
9494
Note:
9595
For Azure OpenAI, requires these environment variables:
@@ -151,7 +151,7 @@ def create_agent(model: str = "anthropic:claude-sonnet-4-0", server_url: str = "
151151

152152
print("✅ Agent created successfully!")
153153

154-
return agent
154+
return agent, access_token
155155

156156

157157
def main():
@@ -175,18 +175,21 @@ def main():
175175
print("\nConnecting to server at http://localhost:8080...")
176176

177177
# Create agent with MCP server connection
178-
agent = create_agent(model=model)
178+
agent, access_token = create_agent(model=model)
179179

180180
# List all available tools from the server using MCP SDK
181-
async def list_tools():
181+
async def list_tools(access_token: str):
182182
"""List all tools available from the MCP server"""
183183
try:
184184
# Import MCP SDK client
185185
from mcp import ClientSession
186186
from mcp.client.sse import sse_client
187187

188-
# Connect using SSE client
189-
async with sse_client("http://localhost:8080/sse") as (read, write):
188+
# Connect using SSE client with authentication
189+
async with sse_client(
190+
"http://localhost:8080/sse",
191+
headers={"Authorization": f"Bearer {access_token}"}
192+
) as (read, write):
190193
async with ClientSession(read, write) as session:
191194
# Initialize the session
192195
await session.initialize()
@@ -214,8 +217,10 @@ async def list_tools():
214217
except Exception as e:
215218
print(f"\n⚠️ Could not list tools: {e}")
216219
print(" The agent will still work with available tools")
220+
import traceback
221+
traceback.print_exc()
217222

218-
asyncio.run(list_tools())
223+
asyncio.run(list_tools(access_token))
219224

220225
# Launch interactive CLI
221226
print("\n" + "=" * 70)
@@ -240,7 +245,7 @@ async def list_tools():
240245
async def _run_cli() -> None:
241246
assert agent is not None
242247
async with agent:
243-
await agent.to_cli(prog_name='proxy-anonymous-agent')
248+
await agent.to_cli(prog_name='proxy-anaconda')
244249

245250
asyncio.run(_run_cli())
246251

0 commit comments

Comments
 (0)