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:
parent
22c7f7476b
commit
bc3619c73c
3 changed files with 3946 additions and 17945 deletions
|
|
@ -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)
|
||||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue