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:
parent
1140815c7b
commit
7b7aa513f5
7 changed files with 328 additions and 272 deletions
13
README.md
13
README.md
|
|
@ -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**:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue