Improve client.js and curl-examples.sh usability

Fix PKCE implementation, SSE parsing, error handling, and usage clarity.
Add --help flag and improve output messaging for both examples.
This commit is contained in:
Basil Hosmer 2025-10-07 14:36:41 -04:00
commit 7b7aa513f5
No known key found for this signature in database
7 changed files with 328 additions and 272 deletions

View file

@ -41,7 +41,9 @@ npm run dev # Start both auth-server and mcp-server
# 4. Test with Inspector
npx -y @modelcontextprotocol/inspector
# Connect to http://localhost:3232/mcp
# (Make sure to include the http:// prefix!)
```
For detailed instructions, see [Installation](#installation).
@ -441,13 +443,12 @@ The script:
- Check that Redis is running (`docker compose ps`)
- Verify the token hasn't expired (tokens last 7 days)
### "Cannot connect to MCP server"
- **Cause**: Incorrect URL or missing path
### "Cannot connect to MCP server" or "Connection Error - Check if your MCP server is running"
- **Cause**: Incorrect URL format or servers not running
- **Solution**:
- Use full URL: `http://localhost:3232/mcp` (not just `:3232`)
- Include the `/mcp` path for MCP endpoints
- Ensure you have a valid Bearer token in the Authorization header
- Use the full URL with http:// prefix: `http://localhost:3232/mcp`
- Ensure **both** auth and MCP servers are running (`npm run dev`)
### "Cannot connect to Docker daemon"
- **Cause**: Docker/OrbStack not running
- **Solution**:

View file

@ -2,7 +2,7 @@
Complete listing of all endpoints provided by each server in the architecture.
## Auth Server (Port 3001)
## Auth Server
Standalone OAuth 2.0 authorization server that handles authentication and token management.
@ -37,7 +37,7 @@ Local simulation of upstream IDP (would be external in production):
---
## MCP Server (Port 3232)
## MCP Server
MCP resource server that implements the Model Context Protocol with delegated authentication.

View file

@ -6,9 +6,9 @@ This document describes different OAuth 2.0 architecture patterns for MCP server
Per the MCP specification, "the authorization server may be hosted with the resource server or as a separate entity." This leads to two primary patterns for implementing OAuth in MCP servers.
## Pattern 1: Separate Authorization Server (Implemented)
## Pattern 1: Separate Authorization Server (Recommended)
The current implementation uses this production-ready pattern with separate authorization and resource servers.
The current implementation demonstrates this pattern with separate authorization and resource servers. (Note: the authorization server is for demonstration purposes only.)
### Architecture
@ -19,7 +19,7 @@ The current implementation uses this production-ready pattern with separate auth
│ │ │ (port 3001) │validate │ (port 3232) │
│ │<────────│ │ │ │
│ │ token │ Issues tokens │ │ Serves MCP │
│ │ │ │ │ resources │
│ │ └──────────────────┘ │ resources │
│ │────────────────────────────────────> │ │
│ │ MCP requests with token │ │
└─────────────┘ └─────────────────┘
@ -27,11 +27,11 @@ The current implementation uses this production-ready pattern with separate auth
### Components
1. **Authorization Server** (port 3001)
1. **Demo Authorization Server** (port 3001)
- Handles OAuth 2.0 authorization flow
- Issues and manages tokens
- Provides token introspection endpoint (RFC 7662)
- Can be replaced with Auth0, Okta, Google OAuth, etc.
- Should be replaced with Auth0, Okta, Google OAuth, etc.
2. **MCP Resource Server** (port 3232)
- Serves MCP protocol resources
@ -39,113 +39,24 @@ The current implementation uses this production-ready pattern with separate auth
- Contains no OAuth authorization code
- Focuses purely on MCP functionality
### Benefits
### Using a Commercial Auth Provider
- **Standards Compliance**: Follows OAuth 2.0 best practices
- **Flexibility**: Easy to swap auth providers
- **Scalability**: Servers scale independently
- **Security**: Clear security boundaries
- **Maintainability**: Separation of concerns
Replacing the demo auth server with a commercial provider:
### Real-World Use Cases
- **SaaS Applications**: Using Auth0 or Okta for authentication
- **Enterprise**: Integrating with corporate SSO (SAML, LDAP)
- **Social Login**: Google, GitHub, Facebook authentication
- **Cloud Native**: AWS Cognito, Azure AD integration
### Implementation Details
The MCP server validates tokens by calling the auth server's `/introspect` endpoint:
```typescript
// In MCP server
const response = await fetch(`${AUTH_SERVER_URL}/introspect`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `token=${accessToken}`
});
const introspection = await response.json();
if (introspection.active) {
// Token is valid, extract user info
const userId = introspection.sub;
}
```
1. **Configure provider**: Set up OAuth app in commercial provider (e.g. Auth0/Okta)
2. **Update metadata URL**: Point to provider's discovery endpoint
3. **Configure introspection**: Set up token validation
4. **Update redirect URIs**: Configure allowed callbacks
5. **Migrate users**: Import existing users if needed
6. **Test integration**: Verify full OAuth flow
---
## Pattern 2: Embedded Authorization Server (Alternative)
## Pattern 2: Embedded Authorization Server (Alternative/Legacy)
An alternative pattern where the OAuth server is embedded within the MCP server. This demonstrates a self-hosted OAuth 2.1 authorization server running in the same process as the MCP server.
The MCP spec describes an alternative pattern where the OAuth server is embedded within the MCP server. However, this pattern is not recommended in the general case, and is not demonstrated in this codebase.
### Architecture
```
┌─────────────┐ ┌─────────────────────────┐
│ │ OAuth │ │
│ MCP Client │────────>│ MCP Server │
│ │ │ ┌──────────────────┐ │
│ │<────────│ │ OAuth Server │ │
│ │ token │ └──────────────────┘ │
│ │ │ ┌──────────────────┐ │
│ │────────>│ │ MCP Resources │ │
│ │ MCP │ └──────────────────┘ │
└─────────────┘ └─────────────────────────┘
```
### Upstream Delegation Pattern
Embedded OAuth often delegates user authentication to an upstream identity provider while maintaining control over token issuance:
```
MCP Client MCP+OAuth Server Upstream IDP
│ │ (Corporate SSO)
│──1. /authorize───────────────>│ │
│<───(show auth page)───────────│ │
│ │ │
│──2. Click "Continue"──────────>│ │
│<──redirect to upstream────────│ │
│ │ │
│──3. /upstream/authorize────────────────────────────────────────>│
│<──(authenticate user)──────────────────────────────────────────│
│──4. Provide credentials────────────────────────────────────────>│
│<──redirect with userId─────────────────────────────────────────│
│ │ │
│──5. /callback with userId───────>│ │
│ │──(validate userId) │
│ │──(issue MCP tokens) │
│<──redirect with auth code──────│ │
│ │ │
│──6. /token (exchange code)─────>│ │
│<──MCP access token─────────────│ │
```
### Characteristics
- **Single Server**: OAuth and MCP in one process
- **Port**: Typically runs on single port (e.g., 3232)
- **Token Validation**: In-process, direct database access
- **Deployment**: Simpler, fewer moving parts
- **Upstream Delegation**: Can delegate authentication to corporate SSO while controlling token issuance
### Benefits
- **Simplicity**: Single server to deploy and manage
- **Performance**: No network hop for token validation
- **Self-Contained**: All functionality in one codebase
- **Control**: Full control over token issuance while leveraging existing identity infrastructure
### Drawbacks
- **Coupling**: Auth and MCP logic intertwined
- **Scalability**: Can't scale auth independently
- **Flexibility**: Harder to switch auth providers
- **Updates**: Auth changes require MCP server updates
### Use Cases
### Possible Use Cases
- **Enterprise Deployments**: Organizations needing control over OAuth token issuance while leveraging existing corporate identity infrastructure (LDAP, Active Directory, SAML)
- **Proof of Concepts**: Quick prototypes and demonstrations
@ -153,39 +64,6 @@ MCP Client MCP+OAuth Server Upstream IDP
- **Isolated Systems**: Air-gapped environments without external connectivity
- **Custom Auth Requirements**: Specialized authentication needs not met by standard providers
### Typical Configuration
Environment variables for embedded pattern:
```bash
BASE_URI=http://localhost:3232 # Single server URL
PORT=3232 # Single port for OAuth + MCP
REDIS_URL=redis://localhost:6379 # Session storage
UPSTREAM_IDP_URL=https://corp.sso # Optional: upstream identity provider
```
### Code Organization
Typical structure for embedded OAuth implementation:
```
src/
├── index.ts # Main entry point (MCP + OAuth + routing)
├── auth/
│ ├── provider.ts # OAuth server implementation
│ └── auth-core.ts # Token generation, PKCE utilities
├── services/
│ ├── mcp.ts # MCP protocol implementation
│ ├── auth.ts # Auth service integration
│ ├── redis-auth.ts # Redis auth operations
│ └── redisTransport.ts # Redis-backed transport
├── handlers/
│ ├── oauth.ts # OAuth endpoints (/authorize, /token)
│ ├── mcp-shttp.ts # Streamable HTTP handler
│ ├── mcp-sse.ts # SSE handler
│ └── upstream-idp.ts # Upstream IDP integration
└── utils/
└── logger.ts # Structured logging
```
---
## Comparison
@ -195,42 +73,11 @@ src/
| **Architecture** | 2+ servers | 1 server |
| **OAuth endpoints** | On auth server | On MCP server |
| **Token validation** | Remote (introspection) | In-process |
| **Deployment** | More complex | Simpler |
| **Scalability** | Independent scaling | Coupled scaling |
| **Provider integration** | Easy (standard APIs) | Difficult |
| **Code organization** | Clear separation | Mixed concerns |
| **Network hops** | Additional for validation | None for validation |
| **Upstream IDP support** | Via auth server | Direct integration |
| **Production readiness** | ✅ Recommended | ⚠️ Limited use cases |
---
## Migration Path
### From Embedded to Separate
If starting with embedded OAuth, migration to separate servers involves:
1. **Extract auth code**: Move OAuth handlers to separate service
2. **Implement introspection**: Add RFC 7662 endpoint
3. **Update MCP server**: Replace in-process validation with introspection calls
4. **Update configuration**: Point MCP server to auth server URL
5. **Migrate upstream IDP**: Move delegation logic to auth server
6. **Test thoroughly**: Ensure token flow works correctly
### From Separate to Commercial Provider
Replacing the demo auth server with a commercial provider:
1. **Configure provider**: Set up OAuth app in Auth0/Okta
2. **Update metadata URL**: Point to provider's discovery endpoint
3. **Configure introspection**: Set up token validation
4. **Update redirect URIs**: Configure allowed callbacks
5. **Migrate users**: Import existing users if needed
6. **Test integration**: Verify full OAuth flow
---
## Best Practices
1. **Use separate servers** for production deployments

View file

@ -194,18 +194,6 @@ This ensures only the client that initiated the flow can exchange the code, even
---
## Architecture Benefits
The separate server architecture provides:
1. **Standards Compliance**: Follows OAuth 2.0 best practices for resource/authorization server separation
2. **Flexibility**: Auth server can be replaced with Auth0, Okta, or other providers
3. **Scalability**: Auth and MCP servers can scale independently based on load
4. **Security**: Token validation via introspection endpoint (RFC 7662)
5. **Maintainability**: Clear separation of authentication and business logic
---
## References
- [RFC 6749: OAuth 2.0 Framework](https://datatracker.ietf.org/doc/html/rfc6749)

View file

@ -24,21 +24,28 @@ Shell script demonstrating API interactions using curl.
- Resource listing and reading
- Prompt operations
**Usage:**
**Three-step workflow:**
```bash
# Make executable
chmod +x curl-examples.sh
# Register client (step 1)
# Step 1: Setup - Register OAuth client and get instructions
./curl-examples.sh
# After getting token (via MCP Inspector)
# 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
# With 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
- Get access token from MCP Inspector or `node client.js`
- Each step explains what to do next
### client.js
Node.js client showing programmatic interaction with the MCP server.
@ -59,6 +66,20 @@ 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.
## Understanding OAuth Tokens
**Authorization Code** vs **Access Token**:
- **Authorization Code**: The temporary code you get from the browser redirect (e.g., `302a80e8...`)
- One-time use only
- Must be exchanged for an access token
- Expires quickly (usually within minutes)
- **Access Token**: The actual bearer token for API authentication (e.g., `mcp_at_...`)
- Used in the `Authorization: Bearer` header
- Valid for 7 days
- What you need for `curl-examples.sh`
## Getting an Access Token
### Option 1: MCP Inspector (Easiest)

View file

@ -1,10 +1,20 @@
#!/usr/bin/env node
/**
* Simple Node.js MCP Client Example
* Simple Node.js MCP Client Example - Manual Implementation
*
* This demonstrates how to interact with the MCP server programmatically.
* It handles the OAuth flow and makes MCP requests.
* This demonstrates how to interact with the MCP server programmatically
* WITHOUT using the MCP SDK client. This is for educational purposes to
* show the underlying protocol mechanics.
*
* In production, you would use the MCP SDK client which handles:
* - SSE (Server-Sent Events) parsing
* - Session management and reconnection logic
* - Request/response correlation
* - Error handling and retries
*
* For SDK usage, see:
* @modelcontextprotocol/sdk/client/streamableHttp.js
*
* Prerequisites:
* - Both servers running: npm run dev
@ -18,6 +28,7 @@ const http = require('http');
const https = require('https');
const { URL } = require('url');
const readline = require('readline');
const crypto = require('crypto');
// Configuration
const AUTH_SERVER = 'http://localhost:3001';
@ -64,11 +75,31 @@ function makeRequest(url, options = {}) {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve({ status: res.statusCode, headers: res.headers, data: json });
} catch {
// Check if this is an SSE response
if (data.startsWith('event:') || data.includes('\ndata: ')) {
// Parse SSE format
const lines = data.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const json = JSON.parse(line.substring(6));
resolve({ status: res.statusCode, headers: res.headers, data: json });
return;
} catch {
// Continue to next line
}
}
}
// If no JSON found in SSE, return raw data
resolve({ status: res.statusCode, headers: res.headers, data });
} else {
// Try to parse as regular JSON
try {
const json = JSON.parse(data);
resolve({ status: res.statusCode, headers: res.headers, data: json });
} catch {
resolve({ status: res.statusCode, headers: res.headers, data });
}
}
});
});
@ -89,23 +120,30 @@ function makeRequest(url, options = {}) {
async function registerClient() {
log.section('Registering OAuth Client');
const response = await makeRequest(`${AUTH_SERVER}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_name: 'node-example-client',
redirect_uris: [REDIRECT_URI]
})
});
try {
const response = await makeRequest(`${AUTH_SERVER}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_name: 'node-example-client',
redirect_uris: [REDIRECT_URI]
})
});
if (response.status === 200 || response.status === 201) {
log.success('Client registered successfully');
return {
clientId: response.data.client_id,
clientSecret: response.data.client_secret
};
} else {
throw new Error(`Registration failed: ${JSON.stringify(response.data)}`);
if (response.status === 200 || response.status === 201) {
log.success('Client registered successfully');
return {
clientId: response.data.client_id,
clientSecret: response.data.client_secret
};
} else {
throw new Error(`Registration failed (HTTP ${response.status}): ${JSON.stringify(response.data)}`);
}
} catch (error) {
if (error.code === 'ECONNREFUSED') {
throw new Error(`Cannot connect to auth server at ${AUTH_SERVER}. Make sure it's running (npm run dev).`);
}
throw error;
}
}
@ -119,16 +157,19 @@ async function performOAuthFlow(clientId, clientSecret) {
log.info('For this demo, we need you to manually complete the OAuth flow.');
log.info('In a production app, this would be automated.\n');
// Generate PKCE challenge (simplified for demo)
const codeVerifier = Buffer.from(Math.random().toString()).toString('base64url');
const codeChallenge = Buffer.from(codeVerifier).toString('base64url');
// Generate PKCE challenge (proper S256 implementation)
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
const authUrl = `${AUTH_SERVER}/authorize?` +
`client_id=${clientId}&` +
`redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
`response_type=code&` +
`code_challenge=${codeChallenge}&` +
`code_challenge_method=plain&` +
`code_challenge_method=S256&` +
`state=demo-state`;
console.log('1. Open this URL in your browser:');
@ -136,7 +177,11 @@ 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\n`);
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('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');
// Get authorization code from user
const rl = readline.createInterface({
@ -145,7 +190,7 @@ async function performOAuthFlow(clientId, clientSecret) {
});
return new Promise((resolve) => {
rl.question('4. Paste the AUTHORIZATION_CODE here: ', async (code) => {
rl.question('5. Paste the AUTHORIZATION_CODE here: ', async (code) => {
rl.close();
// Exchange code for token
@ -182,7 +227,8 @@ async function initializeMCPSession(accessToken) {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
@ -201,8 +247,13 @@ async function initializeMCPSession(accessToken) {
if (response.data.result) {
log.success('MCP session initialized');
// In a real implementation, extract session ID from headers
return response.headers['mcp-session-id'] || 'demo-session-id';
const sessionId = response.headers['mcp-session-id'];
if (!sessionId) {
throw new Error('No session ID received in headers');
}
return sessionId;
} else if (response.data.error) {
throw new Error(`Initialization failed: ${response.data.error.message}`);
} else {
throw new Error(`Initialization failed: ${JSON.stringify(response.data)}`);
}
@ -221,7 +272,8 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
@ -235,6 +287,9 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
toolsResponse.data.result.tools.forEach(tool => {
console.log(` - ${colors.cyan}${tool.name}${colors.reset}: ${tool.description}`);
});
} else if (toolsResponse.data.error) {
log.error(`Failed to list tools: ${toolsResponse.data.error.message}`);
return;
}
// Call the echo tool
@ -244,7 +299,8 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
@ -261,6 +317,8 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
if (echoResponse.data.result) {
console.log('Echo response:', echoResponse.data.result.content);
} else if (echoResponse.data.error) {
log.error(`Echo tool failed: ${echoResponse.data.error.message}`);
}
// Call the add tool
@ -270,7 +328,8 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
@ -285,6 +344,8 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
if (addResponse.data.result) {
console.log('Add result:', addResponse.data.result.content);
} else if (addResponse.data.error) {
log.error(`Add tool failed: ${addResponse.data.error.message}`);
}
// List resources
@ -294,7 +355,8 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
@ -308,6 +370,8 @@ async function demonstrateMCPFeatures(accessToken, sessionId) {
resourcesResponse.data.result.resources.slice(0, 5).forEach(resource => {
console.log(` - ${colors.cyan}${resource.uri}${colors.reset}: ${resource.name}`);
});
} else if (resourcesResponse.data.error) {
log.error(`Failed to list resources: ${resourcesResponse.data.error.message}`);
}
}
@ -335,8 +399,28 @@ async function main() {
log.success('\nExample completed successfully!');
// Show how to use curl-examples.sh as an alternative
console.log(`\n${colors.bright}${colors.cyan}Try curl-examples.sh (alternative to this script):${colors.reset}`);
console.log('─'.repeat(50));
console.log(`\n${colors.green}Your credentials:${colors.reset}`);
console.log(` Access Token: ${colors.yellow}${accessToken}${colors.reset}`);
console.log(` Session ID: ${colors.yellow}${sessionId}${colors.reset}`);
console.log(`\n${colors.cyan}Create a new session with curl:${colors.reset}`);
console.log(` ./examples/curl-examples.sh ${accessToken}`);
console.log(`\n${colors.cyan}Reuse this session with curl:${colors.reset}`);
console.log(` ./examples/curl-examples.sh ${accessToken} ${sessionId}\n`);
} catch (error) {
log.error(`Error: ${error.message}`);
if (error.code === 'ECONNREFUSED') {
log.error('Connection refused - Is the auth server running?');
log.error(`Could not connect to ${error.address || 'server'}:${error.port || 'unknown'}`);
console.log('\nMake sure to start the servers first:');
console.log(' npm run dev');
} else if (error.message) {
log.error(`Error: ${error.message}`);
} else {
log.error(`Error: ${JSON.stringify(error)}`);
}
process.exit(1);
}
}

View file

@ -1,13 +1,27 @@
#!/bin/bash
# =============================================================================
# MCP Server API Examples using curl
# MCP Server API Examples using curl - Manual Implementation
# =============================================================================
#
# This script demonstrates how to interact with the MCP server using curl.
# This script demonstrates how to interact with the MCP server using curl
# WITHOUT using the MCP SDK. This is for educational purposes to show the
# underlying protocol mechanics.
#
# In production, you would use the MCP SDK client which handles:
# - SSE (Server-Sent Events) parsing
# - Session management and reconnection logic
# - Request/response correlation
# - Error handling and retries
#
# Before running, ensure both servers are running: npm run dev
#
# Usage: ./curl-examples.sh [access_token]
# Three-step workflow:
# 1. ./curl-examples.sh → Register OAuth client, get setup instructions
# 2. ./curl-examples.sh <access_token> → Initialize MCP session, get session ID
# 3. ./curl-examples.sh <token> <session> → Run all MCP examples (tools, resources, prompts)
#
# Run with --help for detailed usage information
# =============================================================================
# Configuration
@ -43,6 +57,19 @@ print_info() {
echo -e "${YELLOW}ℹ $1${NC}"
}
# Parse SSE response and extract JSON data
parse_sse_response() {
local RESPONSE="$1"
# Check if response is in SSE format
if echo "$RESPONSE" | grep -q "^event:"; then
# Extract JSON from SSE format (data: line)
echo "$RESPONSE" | grep "^data: " | sed 's/^data: //'
else
# Return as-is if not SSE format
echo "$RESPONSE"
fi
}
# =============================================================================
# OAuth Client Registration
# =============================================================================
@ -64,8 +91,6 @@ register_client() {
print_success "Client registered successfully"
echo "Client ID: $CLIENT_ID"
echo "Client Secret: $CLIENT_SECRET"
echo
echo "Save these credentials - you'll need them for the OAuth flow"
else
print_error "Failed to register client"
echo "Response: $RESPONSE"
@ -80,17 +105,16 @@ register_client() {
initialize_session() {
local ACCESS_TOKEN=$1
print_section "Initializing MCP Session"
if [ -z "$ACCESS_TOKEN" ]; then
print_error "Access token required"
print_info "Get a token using MCP Inspector or implement OAuth flow"
return 1
fi
RESPONSE=$(curl -s -X POST "$MCP_SERVER/mcp" \
RESPONSE=$(curl -s -i -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "init",
@ -105,18 +129,32 @@ initialize_session() {
}
}')
# Extract session ID from response headers (would need -i flag)
# For demonstration, we'll parse from response
echo "Response: $RESPONSE"
# Extract session ID from response headers
SESSION_ID=$(echo "$RESPONSE" | grep -i "^mcp-session-id:" | cut -d' ' -f2 | tr -d '\r\n')
# Extract body from response (SSE format)
BODY=$(echo "$RESPONSE" | sed -n '/^$/,$p' | tail -n +2)
# Parse SSE response if present
if echo "$BODY" | grep -q "^event:"; then
# Extract JSON from SSE format (data: line)
JSON_DATA=$(echo "$BODY" | grep "^data: " | sed 's/^data: //')
else
JSON_DATA="$BODY"
fi
# Check for error
if echo "$RESPONSE" | grep -q "error"; then
if echo "$JSON_DATA" | grep -q "error"; then
print_error "Failed to initialize session"
echo "$JSON_DATA" >&2
return 1
else
print_success "Session initialized"
print_info "Save the Mcp-Session-Id header for subsequent requests"
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"
return 1
fi
}
@ -130,15 +168,19 @@ list_tools() {
print_section "Listing Available Tools"
curl -s -X POST "$MCP_SERVER/mcp" \
RESPONSE=$(curl -s -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "list-tools",
"method": "tools/list"
}' | python3 -m json.tool
}')
# Parse SSE response if needed and pretty print
parse_sse_response "$RESPONSE" | python3 -m json.tool
}
call_echo_tool() {
@ -148,10 +190,11 @@ call_echo_tool() {
print_section "Calling Echo Tool"
curl -s -X POST "$MCP_SERVER/mcp" \
RESPONSE=$(curl -s -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d "{
\"jsonrpc\": \"2.0\",
\"id\": \"echo-1\",
@ -162,7 +205,10 @@ call_echo_tool() {
\"message\": \"$MESSAGE\"
}
}
}" | python3 -m json.tool
}")
# Parse SSE response if needed and pretty print
parse_sse_response "$RESPONSE" | python3 -m json.tool
}
call_add_tool() {
@ -173,10 +219,11 @@ call_add_tool() {
print_section "Calling Add Tool"
curl -s -X POST "$MCP_SERVER/mcp" \
RESPONSE=$(curl -s -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d "{
\"jsonrpc\": \"2.0\",
\"id\": \"add-1\",
@ -188,7 +235,10 @@ call_add_tool() {
\"b\": $B
}
}
}" | python3 -m json.tool
}")
# Parse SSE response if needed and pretty print
parse_sse_response "$RESPONSE" | python3 -m json.tool
}
# =============================================================================
@ -201,15 +251,19 @@ list_resources() {
print_section "Listing Resources (First Page)"
curl -s -X POST "$MCP_SERVER/mcp" \
RESPONSE=$(curl -s -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "list-resources",
"method": "resources/list"
}' | python3 -m json.tool
}')
# Parse SSE response if needed and pretty print
parse_sse_response "$RESPONSE" | python3 -m json.tool
}
read_resource() {
@ -219,10 +273,11 @@ read_resource() {
print_section "Reading Resource: $RESOURCE_URI"
curl -s -X POST "$MCP_SERVER/mcp" \
RESPONSE=$(curl -s -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d "{
\"jsonrpc\": \"2.0\",
\"id\": \"read-resource\",
@ -230,7 +285,10 @@ read_resource() {
\"params\": {
\"uri\": \"$RESOURCE_URI\"
}
}" | python3 -m json.tool
}")
# Parse SSE response if needed and pretty print
parse_sse_response "$RESPONSE" | python3 -m json.tool
}
# =============================================================================
@ -243,15 +301,19 @@ list_prompts() {
print_section "Listing Available Prompts"
curl -s -X POST "$MCP_SERVER/mcp" \
RESPONSE=$(curl -s -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "list-prompts",
"method": "prompts/list"
}' | python3 -m json.tool
}')
# Parse SSE response if needed and pretty print
parse_sse_response "$RESPONSE" | python3 -m json.tool
}
get_prompt() {
@ -261,10 +323,11 @@ get_prompt() {
print_section "Getting Prompt: $PROMPT_NAME"
curl -s -X POST "$MCP_SERVER/mcp" \
RESPONSE=$(curl -s -X POST "$MCP_SERVER/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d "{
\"jsonrpc\": \"2.0\",
\"id\": \"get-prompt\",
@ -272,14 +335,46 @@ get_prompt() {
\"params\": {
\"name\": \"$PROMPT_NAME\"
}
}" | python3 -m json.tool
}")
# Parse SSE response if needed and pretty print
parse_sse_response "$RESPONSE" | python3 -m json.tool
}
# =============================================================================
# Main Script
# =============================================================================
show_usage() {
echo -e "${BLUE}MCP Server API Examples - Usage${NC}"
echo "================================="
echo
echo "This script demonstrates MCP API interactions in three steps:"
echo
echo -e " ${GREEN}Step 1: Register OAuth client${NC}"
echo " ./curl-examples.sh"
echo " → Registers a client and shows how to get an access token"
echo
echo -e " ${GREEN}Step 2: Initialize MCP session${NC}"
echo " ./curl-examples.sh <access_token>"
echo " → Creates an MCP session and returns a session ID"
echo
echo -e " ${GREEN}Step 3: Run MCP examples${NC}"
echo " ./curl-examples.sh <access_token> <session_id>"
echo " → Demonstrates tools, resources, and prompts"
echo
echo "Options:"
echo " --help, -h Show this help message"
echo
}
main() {
# Check for help flag
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
show_usage
exit 0
fi
echo -e "${BLUE}MCP Server API Examples${NC}"
echo "================================="
@ -288,26 +383,46 @@ main() {
SESSION_ID=$2
if [ -z "$ACCESS_TOKEN" ]; then
print_info "No access token provided"
print_info "Usage: $0 <access_token> [session_id]"
print_section "Getting Started"
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..."
echo
print_section "Step 1: Register OAuth Client"
register_client
echo
print_info "To continue, you need to:"
print_info "1. Complete OAuth flow to get an access token"
print_info "2. Run: $0 <your_access_token>"
print_info ""
print_info "Use MCP Inspector for easy OAuth flow:"
print_info "npx -y @modelcontextprotocol/inspector"
print_info "Connect to: http://localhost:3232/mcp"
print_section "Next Steps"
print_info "Now you need to get an access token. You have two options:"
echo
print_info "Option 1: Use MCP Inspector (recommended):"
print_info " npx -y @modelcontextprotocol/inspector"
print_info " Connect to: http://localhost:3232/mcp"
print_info " Complete the OAuth flow in the Auth tab"
echo
print_info "Option 2: Use the client.js example:"
print_info " node examples/client.js"
echo
print_info "Once you have an access token, run:"
print_info " $0 <your_access_token>"
exit 0
fi
# If we have a token, demonstrate API calls
# If we have a token but no session, initialize session
if [ -z "$SESSION_ID" ]; then
initialize_session "$ACCESS_TOKEN"
print_info "Rerun with: $0 $ACCESS_TOKEN <session_id>"
print_section "Initializing MCP Session"
RETURNED_SESSION_ID=$(initialize_session "$ACCESS_TOKEN")
if [ $? -eq 0 ] && [ -n "$RETURNED_SESSION_ID" ]; then
echo
print_success "MCP session initialized successfully"
print_info "Session ID: $RETURNED_SESSION_ID"
echo
print_info "To run all MCP examples (tools, resources, prompts), use:"
echo -e " ${CYAN}$0 $ACCESS_TOKEN $RETURNED_SESSION_ID${NC}"
echo
print_info "For help:"
echo -e " ${CYAN}$0 --help${NC}"
fi
exit 0
fi