Unify auth and MCP servers into single modular codebase

- Merge auth-server and mcp-server into unified architecture under src/modules/
- Add configurable auth modes: internal (single process) and external (separate processes)
- Update example scripts to support OAuth discovery and both auth modes
- Consolidate OAuth documentation into comprehensive implementation guide
- Add configuration options for development vs production deployments
- Support both Redis-backed and in-memory session storage
This commit is contained in:
Basil Hosmer 2025-10-14 00:35:21 -04:00
commit bc3619c73c
No known key found for this signature in database
3 changed files with 3946 additions and 17945 deletions

View file

@ -1,98 +0,0 @@
# MCP Server Examples
These examples demonstrate interaction with the MCP server, covering OAuth authentication, session management, and MCP operations (tools, resources, prompts).
**Authentication is the most complex part** of using an MCP server with OAuth. The examples demonstrate authentication interactions using two different step-by-step approaches:
- **client.js**: runs the server's end-to-end auth flow in the browser
- **curl-examples.sh**: uses raw HTTP interactions (requires obtaining an access token separately)
See below for details.
## Prerequisites
Before running any examples, ensure:
1. Redis is running: `docker compose up -d`
2. Both servers are running: `npm run dev`
3. Servers are accessible at:
- Auth Server: http://localhost:3001
- MCP Server: http://localhost:3232
## Available Examples
### client.js
Node.js client showing programmatic interaction with the MCP server.
**Features:**
- Complete OAuth flow demonstration
- Automatic client registration
- MCP session management
- Tool and resource operations
**Usage:**
```bash
# Run the example
node client.js
# Or make it executable
chmod +x client.js
./client.js
```
**Note:** When you complete the OAuth flow in your browser, you'll be redirected to `http://localhost:8080/callback` which will show "site can't be reached". This is expected! Simply copy the authorization code from the URL in your browser's address bar (the long string after `code=`). The script will exchange this for an access token and display it for use with other tools.
### curl-examples.sh
Shell script demonstrating API interactions using curl.
**Features:**
- OAuth client registration
- MCP session initialization
- Tool calls (echo, add)
- Resource listing and reading
- Prompt operations
**Three-step workflow:**
```bash
# Make executable
chmod +x curl-examples.sh
# Step 1: Setup - Register OAuth client and get instructions
./curl-examples.sh
# Step 2: Create session - Get an access token (via Inspector or client.js), then:
./curl-examples.sh YOUR_ACCESS_TOKEN
# → Initializes MCP session and displays session ID
# Step 3: Run examples - Use both access token and session ID:
./curl-examples.sh YOUR_ACCESS_TOKEN YOUR_SESSION_ID
# → Demonstrates all MCP features (tools, resources, prompts)
```
**Quick reference:**
- Run `./curl-examples.sh --help` for detailed usage
- Requires an access token (get from MCP Inspector or `node client.js`)
- Each step explains what to do next
## Troubleshooting
### "401 Unauthorized"
- Token may be expired (7-day TTL)
- Token may be invalid
- Get a new token via OAuth flow
### "Session not found"
- Session expired (5-minute TTL)
- Re-initialize with the same token
### "Cannot connect"
- Ensure servers are running: `npm run dev`
- Check Redis is running: `docker compose ps`
- Verify URLs are correct
## Additional Resources
- [MCP Protocol Documentation](https://modelcontextprotocol.io)
- [OAuth Flow Documentation](../docs/oauth-flow.md)
- [API Endpoints Reference](../docs/endpoints.md)
- [Main README](../README.md)

View file

@ -24,16 +24,19 @@
* node client.js
*/
const http = require('http');
const https = require('https');
const { URL } = require('url');
const readline = require('readline');
const crypto = require('crypto');
import http from 'http';
import https from 'https';
import { URL } from 'url';
import readline from 'readline';
import crypto from 'crypto';
// Configuration
const AUTH_SERVER = 'http://localhost:3001';
const MCP_SERVER = 'http://localhost:3232';
const REDIRECT_URI = 'http://localhost:8080/callback';
const REDIRECT_URI = 'http://localhost:3232/callback';
// AUTH_SERVER will be discovered dynamically from OAuth metadata
// This allows the client to work with both internal and external auth modes
let AUTH_SERVER = null;
// Colors for console output
const colors = {
@ -114,6 +117,43 @@ function makeRequest(url, options = {}) {
});
}
/**
* Discover OAuth endpoints from metadata
*/
async function discoverOAuthMetadata() {
log.section('Discovering OAuth Configuration');
try {
const response = await makeRequest(`${MCP_SERVER}/.well-known/oauth-authorization-server`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (response.status === 200 && response.data) {
log.success('OAuth metadata retrieved');
// Extract the base URL from the issuer or authorization_endpoint
const authEndpoint = response.data.authorization_endpoint;
if (authEndpoint) {
const url = new URL(authEndpoint);
AUTH_SERVER = `${url.protocol}//${url.host}`;
log.info(`Auth server discovered at: ${AUTH_SERVER}`);
} else {
throw new Error('No authorization_endpoint found in metadata');
}
return response.data;
} else {
throw new Error(`Metadata discovery failed (HTTP ${response.status})`);
}
} catch (error) {
if (error.code === 'ECONNREFUSED') {
throw new Error(`Cannot connect to MCP server at ${MCP_SERVER}. Make sure it's running (npm run dev).`);
}
throw error;
}
}
/**
* Step 1: Register OAuth client
*/
@ -178,7 +218,7 @@ async function performOAuthFlow(clientId, clientSecret) {
console.log('2. Complete the authentication flow');
console.log('3. You\'ll be redirected to a URL like:');
console.log(` ${REDIRECT_URI}?code=AUTHORIZATION_CODE&state=demo-state`);
console.log(` ${colors.yellow}(You'll see "site can't be reached" - this is expected!)${colors.reset}\n`);
console.log(` ${colors.yellow}(You'll see "Cannot GET /callback" - this is expected!)${colors.reset}\n`);
console.log('4. Copy the authorization code from the URL in your browser\'s address bar');
console.log(' The code is the long string after "code=" and before "&state="\n');
@ -260,7 +300,7 @@ async function initializeMCPSession(accessToken) {
}
/**
* Step 4: Demonstrate MCP features
* Step 4: Demonstrate MCP features (tools, resources, prompts)
*/
async function demonstrateMCPFeatures(accessToken, sessionId) {
log.section('Demonstrating MCP Features');
@ -373,6 +413,61 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
} else if (resourcesResponse.data.error) {
log.error(`Failed to list resources: ${resourcesResponse.data.error.message}`);
}
// List prompts
log.info('\nFetching available prompts...');
const promptsResponse = await makeRequest(`${MCP_SERVER}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'list-prompts',
method: 'prompts/list'
})
});
if (promptsResponse.data.result && promptsResponse.data.result.prompts) {
console.log(`Found ${promptsResponse.data.result.prompts.length} prompts:`);
promptsResponse.data.result.prompts.forEach(prompt => {
console.log(` - ${colors.cyan}${prompt.name}${colors.reset}: ${prompt.description}`);
});
} else if (promptsResponse.data.error) {
log.error(`Failed to list prompts: ${promptsResponse.data.error.message}`);
}
// Get a specific prompt
log.info('\nGetting simple_prompt...');
const getPromptResponse = await makeRequest(`${MCP_SERVER}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'get-prompt',
method: 'prompts/get',
params: {
name: 'simple_prompt'
}
})
});
if (getPromptResponse.data.result && getPromptResponse.data.result.messages) {
console.log('Prompt messages:');
getPromptResponse.data.result.messages.forEach(message => {
console.log(` [${message.role}]: ${message.content.text || message.content}`);
});
} else if (getPromptResponse.data.error) {
log.error(`Failed to get prompt: ${getPromptResponse.data.error.message}`);
}
}
/**
@ -383,6 +478,9 @@ async function main() {
console.log('==========================\n');
try {
// Step 0: Discover OAuth metadata
await discoverOAuthMetadata();
// Step 1: Register client
const { clientId, clientSecret } = await registerClient();
console.log(`Client ID: ${clientId}`);

View file

@ -25,10 +25,11 @@
# =============================================================================
# Configuration
AUTH_SERVER="http://localhost:3001"
MCP_SERVER="http://localhost:3232"
CLIENT_NAME="curl-example-client"
REDIRECT_URI="http://localhost:3000/callback"
# AUTH_SERVER will be discovered from OAuth metadata
AUTH_SERVER=""
# Colors for output
RED='\033[0;31m'
@ -70,6 +71,30 @@ parse_sse_response() {
fi
}
# =============================================================================
# OAuth Metadata Discovery
# =============================================================================
discover_oauth_metadata() {
print_section "Discovering OAuth Configuration"
RESPONSE=$(curl -s "$MCP_SERVER/.well-known/oauth-authorization-server")
# Extract authorization endpoint to determine auth server
AUTH_ENDPOINT=$(echo "$RESPONSE" | grep -o '"authorization_endpoint":"[^"]*' | cut -d'"' -f4)
if [ -n "$AUTH_ENDPOINT" ]; then
# Extract base URL from authorization endpoint
AUTH_SERVER=$(echo "$AUTH_ENDPOINT" | sed -E 's|(https?://[^/]+).*|\1|')
print_success "OAuth metadata retrieved"
print_info "Auth server discovered at: $AUTH_SERVER"
else
print_error "Failed to discover OAuth metadata"
echo "Response: $RESPONSE"
exit 1
fi
}
# =============================================================================
# OAuth Client Registration
# =============================================================================
@ -111,6 +136,8 @@ initialize_session() {
return 1
fi
print_info "Sending initialization request to $MCP_SERVER/mcp..." >&2
RESPONSE=$(curl -s -i -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
@ -143,17 +170,30 @@ initialize_session() {
JSON_DATA="$BODY"
fi
# Check for error
if echo "$JSON_DATA" | grep -q "error"; then
print_error "Failed to initialize session"
echo "$JSON_DATA" >&2
# Check for authentication error first
if echo "$RESPONSE" | head -n 1 | grep -q "401"; then
print_error "Authentication failed - token is not valid" >&2
if echo "$JSON_DATA" | grep -q '"error"'; then
echo "$JSON_DATA" | python3 -m json.tool >&2 2>/dev/null || echo "$JSON_DATA" >&2
fi
return 1
fi
# Check for other errors
if echo "$JSON_DATA" | grep -q '"error"'; then
print_error "Failed to initialize session - server returned error:" >&2
echo "$JSON_DATA" | python3 -m json.tool >&2 2>/dev/null || echo "$JSON_DATA" >&2
return 1
elif [ -n "$SESSION_ID" ]; then
# Return session ID on stdout (for capture by caller)
echo "$SESSION_ID"
return 0
else
print_error "No session ID received"
print_error "Failed to initialize session - no session ID received" >&2
if [ -n "$BODY" ]; then
print_info "Response body:" >&2
echo "$BODY" | head -n 10 >&2
fi
return 1
fi
}
@ -387,8 +427,9 @@ main() {
print_info "This script requires an OAuth access token to demonstrate MCP operations."
print_info "Since you don't have one yet, let's help you get started!"
echo
print_info "First, we'll register an OAuth client for you..."
print_info "First, we'll discover the OAuth server and register a client for you..."
echo
discover_oauth_metadata
register_client
echo
print_section "Next Steps"