Skip to content
Merged
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
22 changes: 20 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,20 +288,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({

### Authentication Flow

**Two Types of Auth**:
**Three Types of Auth**:

1. **Bearer Token** (AUTH_TOKEN):
- Static token for Lambda authentication
- Generated during deployment
- Validates client requests to Lambda function
- Stored in environment variable
- Required for `/mcp` JSON-RPC endpoint

2. **OAuth Tokens** (Strava):
2. **Authless Mode** (ALLOW_AUTHLESS):
- When `ALLOW_AUTHLESS=true` (default), SSE endpoints bypass auth
- Enables Claude.ai custom connectors (which don't support custom headers)
- Applies to: `/sse`, `/sse/`, `/message` endpoints
- Does NOT apply to: `/mcp` endpoint (still requires Bearer token)
- Security: Keep Lambda URL private when enabled

3. **OAuth Tokens** (Strava):
- Access token (short-lived, ~6 hours)
- Refresh token (long-lived)
- Automatically managed by StravaClient
- Stored in environment variables (CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)

**Authentication Middleware Logic**:
```typescript
// Simplified auth flow
if (path === '/health') → bypass auth
if (ALLOW_AUTHLESS && (path === '/sse' || path === '/message')) → bypass auth
if (path === '/message' && valid session) → bypass auth
else → require Bearer token
```
Comment thread
Stealinglight marked this conversation as resolved.

### Local Development Workflow

```bash
Expand Down Expand Up @@ -539,6 +556,7 @@ sam delete --stack-name strava-mcp-stack # Delete deployment
- `STRAVA_CLIENT_SECRET` - Strava API client secret
- `STRAVA_REFRESH_TOKEN` - Strava OAuth refresh token
- `AUTH_TOKEN` - Bearer token for Lambda authentication
- `ALLOW_AUTHLESS` - When "true", SSE endpoints bypass auth (default: "true")
- `NODE_ENV` - Environment (production/development)

### File Locations
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,32 @@ Need to see the config again? Run:
bun run deploy:show-config
```

### 5. Claude.ai Custom Connectors (Web/Mobile)

Claude.ai custom connectors use **authless mode** via SSE transport. This is enabled by default (`ALLOW_AUTHLESS=true`).

**Setup Steps:**
1. Go to **Claude.ai** → **Settings** → **Connectors** → **Add custom connector**
2. Enter just the **base URL** (no path): `https://your-function-url.lambda-url.us-east-1.on.aws`
3. Claude.ai will automatically discover the `/sse` endpoint and connect
4. Verify tools appear and can be invoked

**Important Notes:**
- Claude.ai does NOT support custom headers (no Bearer token auth)
- Authless mode only applies to SSE endpoints (`/sse`, `/message`)
- The `/mcp` JSON-RPC endpoint still requires Bearer token authentication
- Keep your Lambda Function URL private for security

**Security Considerations:**
- Authless mode is **high risk** and should be treated as **personal-use-only**
- If authless mode is enabled, anyone with your Lambda URL can access and act on your Strava data
- Authless mode is **not recommended for production**; if you must use it, combine it with stricter controls such as AWS WAF IP allowlisting or an additional shared secret

**Recommended secure deployment (authless disabled, Bearer tokens required everywhere):**
```bash
sam deploy --parameter-overrides AllowAuthless=false
```

## Documentation

📚 **[Full Documentation](https://stealinglight.github.io/StravaMCP)**
Expand Down
94 changes: 93 additions & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ The Lambda function uses **Bearer token authentication** to secure access to you
- ✅ Works with Claude web, mobile, and desktop
- ✅ Easy to rotate tokens when needed

## How It Works
## Authentication Modes

The server supports two authentication modes:

### 1. Bearer Token Authentication (Default for `/mcp` endpoint)

For Claude Desktop and other MCP clients that support custom headers.

```
┌──────────────┐
Expand All @@ -31,6 +37,33 @@ The Lambda function uses **Bearer token authentication** to secure access to you
└──────────────┘
```

### 2. Authless Mode (For Claude.ai Custom Connectors)

Claude.ai custom connectors only support authless or OAuth 2.1 (DCR) connections. Since OAuth 2.1 with DCR is complex to implement, this server provides an **authless mode** for SSE endpoints.

When `ALLOW_AUTHLESS=true` (default), the SSE transport endpoints (`/sse`, `/sse/`, `/message`) bypass authentication, allowing Claude.ai to connect without Bearer tokens.

```
┌──────────────┐
│ Claude.ai │
│ (Web/Mobile) │
└──────┬───────┘
│ SSE connection (no auth required)
┌──────────────┐
│ Lambda │─── ALLOW_AUTHLESS=true
│ (Middleware)│ Skips auth for SSE
└──────┬───────┘
│ Direct access
┌──────────────┐
│ MCP Server │
│ (Strava API) │
└──────────────┘
```

**Important**: The `/mcp` JSON-RPC endpoint still requires Bearer token authentication for backward compatibility with other clients.

## Setup Steps

### 1. Deploy and Auto-Generate AUTH_TOKEN
Expand Down Expand Up @@ -116,6 +149,20 @@ Add as a Custom Connector in Claude Settings:

**Important**: The URL must end with `/mcp` - this is the MCP endpoint.

#### Claude.ai Custom Connectors (Authless Mode)

For Claude.ai web and mobile, use the authless SSE transport:

1. Go to **Claude.ai** → **Settings** → **Connectors** → **Add custom connector**
2. Enter just the **base URL** (no `/mcp` or `/sse` path):
```
https://your-function-url.lambda-url.us-east-1.on.aws
```
3. Claude.ai will automatically discover the `/sse` endpoint
4. Verify tools appear in the conversation

**Note**: Authless mode is enabled by default (`ALLOW_AUTHLESS=true`). Claude.ai cannot send custom headers, so Bearer token auth is not supported for Claude.ai connectors.

### 4. Test Authentication

Test that authentication is working:
Expand Down Expand Up @@ -171,6 +218,51 @@ To rotate your authentication token:
- Use short or predictable tokens
- Reuse tokens across multiple services

## Authless Mode Configuration

### Enabling Authless Mode (Default)

Authless mode is **enabled by default** for Claude.ai compatibility. The `ALLOW_AUTHLESS` parameter is set to `"true"` in the SAM template.

When enabled:
- SSE endpoints (`/sse`, `/sse/`, `/message`) do not require authentication
- The `/mcp` JSON-RPC endpoint still requires Bearer token authentication
- Claude.ai custom connectors can connect using the base URL

### Disabling Authless Mode

To require Bearer token authentication for all endpoints:

```bash
# During deployment
sam deploy --parameter-overrides AllowAuthless=false

# Or update samconfig.toml
[default.deploy.parameters]
parameter_overrides = [
"AllowAuthless=false",
# ... other parameters
]
```

When disabled:
- All endpoints require Bearer token authentication
- Claude.ai custom connectors will NOT work (they cannot send custom headers)
- Only Claude Desktop and clients supporting custom headers can connect

### Security Implications

**Authless mode enabled (`ALLOW_AUTHLESS=true`, advanced / opt-in):**
- ⚠️ Not enabled by default. Enabling this on a publicly accessible Lambda URL exposes `/sse`, `/sse/`, and `/message` without authentication.
- ⚠️ Anyone who learns your Lambda URL (via logs, screenshots, or misconfiguration) can access your Strava-backed tools via SSE; URL secrecy alone is **not** sufficient protection.
- ✅ Only enable for strictly local or personal use where you fully control access to the URL.
- 💡 If you must use authless mode in a cloud environment, combine it with additional controls such as AWS WAF/IP allowlisting and/or an extra shared secret (e.g., a query parameter or header checked by your Lambda).

**Authless mode disabled (`ALLOW_AUTHLESS=false`, recommended default):**
- ✅ All requests require a valid Bearer token
- ❌ Claude.ai custom connectors won't work
- ✅ Maximum security for shared, team, or production environments

## Security Considerations

### What This Protects
Expand Down
4 changes: 4 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const envSchema = z.object({
STRAVA_REFRESH_TOKEN: z.string().min(1, 'STRAVA_REFRESH_TOKEN is required'),
AUTH_TOKEN: z.string().min(32, 'AUTH_TOKEN must be at least 32 characters'),
PORT: z.string().default('3000').transform(Number),
// ALLOW_AUTHLESS: When "true", bypasses auth for SSE endpoints (/sse, /sse/, /message)
// This enables Claude.ai custom connectors which don't support Bearer token auth
ALLOW_AUTHLESS: z.string().default('true').transform((val) => val.toLowerCase() === 'true'),
});

/**
Expand All @@ -25,6 +28,7 @@ export function getConfig() {
STRAVA_REFRESH_TOKEN: process.env.STRAVA_REFRESH_TOKEN,
AUTH_TOKEN: process.env.AUTH_TOKEN,
PORT: process.env.PORT,
ALLOW_AUTHLESS: process.env.ALLOW_AUTHLESS,
});

if (!result.success) {
Expand Down
74 changes: 59 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ try {
const server = new Server(
{
name: 'strava-mcp-server',
version: '1.0.0',
version: '3.0.0',
},
{
capabilities: {
Expand Down Expand Up @@ -325,50 +325,91 @@ async function main() {

// Authentication middleware for local dev (optional - can be disabled)
// Supports both Authorization header and query parameter
// When ALLOW_AUTHLESS=true, SSE endpoints bypass auth for Claude.ai custom connectors
app.use((req: Request, res: Response, next) => {
if (req.path === '/health') {
if (req.path === '/health' || req.path === '/debug') {
return next();
}

// For SSE endpoint, authentication happens via query param or header
if (req.path === '/sse' || req.path === '/sse/') {
// SSE authentication happens here before establishing connection
// Continue to token validation below
// Check if authless mode is enabled for SSE endpoints
// This allows Claude.ai custom connectors to connect without Bearer tokens
const isSSEEndpoint = req.path === '/sse' || req.path === '/sse/';
const isMessageEndpoint = req.path === '/message';

if (config.ALLOW_AUTHLESS && (isSSEEndpoint || isMessageEndpoint)) {
if (isSSEEndpoint) {
console.error('[StravaServer] Authless SSE connection allowed (ALLOW_AUTHLESS=true)');
}
return next();
}

// For SSE message endpoint, trust valid session IDs
// For SSE message endpoint with valid session, trust the session
const sessionId = req.query.sessionId as string;
if (req.path === '/message' && sessionId && transports[sessionId]) {
if (isMessageEndpoint && sessionId && transports[sessionId]) {
return next();
}

// For local development, authentication is optional
// If AUTH_TOKEN is set in .env, validate it
if (config.AUTH_TOKEN && config.AUTH_TOKEN.length > 0) {
let token: string | undefined;

const authHeader = req.headers['authorization'];
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
} else if (req.query.token) {
token = req.query.token as string;
}

if (!token || token !== config.AUTH_TOKEN) {
console.error('[StravaServer] Invalid or missing token');
return res.status(401).json({
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or missing token'
});
}
}

next();
});

// Health check endpoint
// Health check endpoint - enhanced with diagnostic info
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'healthy', version: '2.0.0' });
res.json({
status: 'healthy',
version: '3.0.0',
runtime: 'local',
authless: config?.ALLOW_AUTHLESS ?? false,
timestamp: new Date().toISOString(),
});
});

// Debug endpoint - helps troubleshoot Claude.ai connection issues
app.get('/debug', (_req: Request, res: Response) => {
res.json({
status: 'ok',
version: '3.0.0',
authless_enabled: config?.ALLOW_AUTHLESS ?? false,
environment: process.env.NODE_ENV || 'development',
endpoints: {
health: '/health',
debug: '/debug',
sse: '/sse (GET, establishes SSE connection)',
message: '/message (POST, requires sessionId query param)',
mcp: '/mcp (POST, requires Bearer token)',
},
claude_ai_setup: {
connector_url: `Use base URL only: http://localhost:${config.PORT}`,
auth_mode: config?.ALLOW_AUTHLESS ? 'Authless (SSE endpoints bypass auth)' : 'Bearer token required',
transport: 'SSE',
note: 'For local testing with Claude.ai, use ngrok or similar to expose localhost',
},
sse_headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
});

// SSE endpoint - establishes the server-to-client event stream
Expand Down Expand Up @@ -453,7 +494,7 @@ async function main() {
},
serverInfo: {
name: 'strava-mcp-server',
version: '2.0.0',
version: '3.0.0',
},
};
break;
Expand Down Expand Up @@ -674,7 +715,10 @@ async function main() {
app.listen(port, () => {
console.error(`[StravaServer] Remote MCP server running on http://localhost:${port}`);
console.error(`[StravaServer] MCP endpoint: http://localhost:${port}/mcp`);
console.error(`[StravaServer] SSE endpoint: http://localhost:${port}/sse`);
console.error(`[StravaServer] Health check: http://localhost:${port}/health`);
console.error(`[StravaServer] Debug info: http://localhost:${port}/debug`);
console.error(`[StravaServer] Authless mode: ${config.ALLOW_AUTHLESS ? 'ENABLED' : 'DISABLED'}`);
});
}

Expand Down
Loading