example-remote-server/examples/client.js
Basil Hosmer c03a9b4e77
Remove embedded OAuth implementation and flatten directory structure
Simplified the repository to have a single OAuth pattern with separate auth and MCP servers at the top level. Preserved embedded OAuth documentation as an alternative pattern.
2025-10-06 21:30:14 -04:00

345 lines
No EOL
9.5 KiB
JavaScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Simple Node.js MCP Client Example
*
* This demonstrates how to interact with the MCP server programmatically.
* It handles the OAuth flow and makes MCP requests.
*
* Prerequisites:
* - Both servers running: npm run dev
* - Node.js installed
*
* Usage:
* node client.js
*/
const http = require('http');
const https = require('https');
const { URL } = require('url');
const readline = require('readline');
// Configuration
const AUTH_SERVER = 'http://localhost:3001';
const MCP_SERVER = 'http://localhost:3232';
const REDIRECT_URI = 'http://localhost:8080/callback';
// Colors for console output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
// Utility functions
const log = {
info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
section: (msg) => console.log(`\n${colors.bright}${colors.blue}=== ${msg} ===${colors.reset}`)
};
/**
* Simple HTTP request helper
*/
function makeRequest(url, options = {}) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const client = parsedUrl.protocol === 'https:' ? https : http;
const reqOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + parsedUrl.search,
method: options.method || 'GET',
headers: options.headers || {}
};
const req = client.request(reqOptions, (res) => {
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 {
resolve({ status: res.statusCode, headers: res.headers, data });
}
});
});
req.on('error', reject);
if (options.body) {
req.write(options.body);
}
req.end();
});
}
/**
* Step 1: Register OAuth client
*/
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]
})
});
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)}`);
}
}
/**
* Step 2: Simple OAuth flow (manual for demonstration)
* In production, use a proper OAuth library
*/
async function performOAuthFlow(clientId, clientSecret) {
log.section('OAuth Authorization Flow');
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');
const authUrl = `${AUTH_SERVER}/authorize?` +
`client_id=${clientId}&` +
`redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
`response_type=code&` +
`code_challenge=${codeChallenge}&` +
`code_challenge_method=plain&` +
`state=demo-state`;
console.log('1. Open this URL in your browser:');
console.log(` ${colors.cyan}${authUrl}${colors.reset}\n`);
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`);
// Get authorization code from user
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question('4. Paste the AUTHORIZATION_CODE here: ', async (code) => {
rl.close();
// Exchange code for token
log.info('Exchanging authorization code for access token...');
const tokenResponse = await makeRequest(`${AUTH_SERVER}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=authorization_code&` +
`code=${code}&` +
`redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
`client_id=${clientId}&` +
`client_secret=${clientSecret}&` +
`code_verifier=${codeVerifier}`
});
if (tokenResponse.data.access_token) {
log.success('Access token obtained!');
resolve(tokenResponse.data.access_token);
} else {
throw new Error(`Token exchange failed: ${JSON.stringify(tokenResponse.data)}`);
}
});
});
}
/**
* Step 3: Initialize MCP session
*/
async function initializeMCPSession(accessToken) {
log.section('Initializing MCP Session');
const response = await makeRequest(`${MCP_SERVER}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'init',
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: {
name: 'node-example',
version: '1.0'
}
}
})
});
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';
} else {
throw new Error(`Initialization failed: ${JSON.stringify(response.data)}`);
}
}
/**
* Step 4: Demonstrate MCP features
*/
async function demonstrateMCPFeatures(accessToken, sessionId) {
log.section('Demonstrating MCP Features');
// List available tools
log.info('Fetching available tools...');
const toolsResponse = await makeRequest(`${MCP_SERVER}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'list-tools',
method: 'tools/list'
})
});
if (toolsResponse.data.result && toolsResponse.data.result.tools) {
console.log(`Found ${toolsResponse.data.result.tools.length} tools:`);
toolsResponse.data.result.tools.forEach(tool => {
console.log(` - ${colors.cyan}${tool.name}${colors.reset}: ${tool.description}`);
});
}
// Call the echo tool
log.info('\nCalling echo tool...');
const echoResponse = await makeRequest(`${MCP_SERVER}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'echo-1',
method: 'tools/call',
params: {
name: 'echo',
arguments: {
message: 'Hello from Node.js client!'
}
}
})
});
if (echoResponse.data.result) {
console.log('Echo response:', echoResponse.data.result.content);
}
// Call the add tool
log.info('\nCalling add tool (5 + 3)...');
const addResponse = await makeRequest(`${MCP_SERVER}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'add-1',
method: 'tools/call',
params: {
name: 'add',
arguments: { a: 5, b: 3 }
}
})
});
if (addResponse.data.result) {
console.log('Add result:', addResponse.data.result.content);
}
// List resources
log.info('\nFetching available resources...');
const resourcesResponse = await makeRequest(`${MCP_SERVER}/mcp`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Mcp-Session-Id': sessionId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'list-resources',
method: 'resources/list'
})
});
if (resourcesResponse.data.result && resourcesResponse.data.result.resources) {
console.log(`Found ${resourcesResponse.data.result.resources.length} resources (showing first 5):`);
resourcesResponse.data.result.resources.slice(0, 5).forEach(resource => {
console.log(` - ${colors.cyan}${resource.uri}${colors.reset}: ${resource.name}`);
});
}
}
/**
* Main function
*/
async function main() {
console.log(`${colors.bright}${colors.blue}MCP Node.js Client Example${colors.reset}`);
console.log('==========================\n');
try {
// Step 1: Register client
const { clientId, clientSecret } = await registerClient();
console.log(`Client ID: ${clientId}`);
console.log(`Client Secret: ${clientSecret}\n`);
// Step 2: OAuth flow
const accessToken = await performOAuthFlow(clientId, clientSecret);
// Step 3: Initialize MCP session
const sessionId = await initializeMCPSession(accessToken);
// Step 4: Demonstrate features
await demonstrateMCPFeatures(accessToken, sessionId);
log.success('\nExample completed successfully!');
} catch (error) {
log.error(`Error: ${error.message}`);
process.exit(1);
}
}
// Run the example
main();