Skip to main content
Glama

MCP Binance

by shukehi
dev-mode.md53.4 kB
# ChatGPT MCP Developer Mode MCP - Complete Tutorial ## Table of Contents 1. [Introduction](#introduction) 2. [What's New in Developer Mode](#whats-new-in-developer-mode) 3. [Requirements & Limitations](#requirements--limitations) 4. [Option 1: Non-OAuth Implementation](#option-1-non-oauth-implementation) 5. [Option 2: OAuth Implementation with Supabase](#option-2-oauth-implementation-with-supabase) 6. [Deployment Options](#deployment-options) 7. [Common Gotchas & Solutions](#common-gotchas--solutions) 8. [Testing Your MCP Server](#testing-your-mcp-server) 9. [Troubleshooting](#troubleshooting) ## Introduction ChatGPT's Developer Mode with MCP Server Tools support was officially announced on **September 9th, 2025**, marking a significant milestone in AI-developer integration. This feature enables ChatGPT to interact with custom MCP (Model Context Protocol) servers, providing unprecedented extensibility and real-world integration capabilities. ### What is MCP? Model Context Protocol (MCP) is an open protocol that standardizes how AI assistants connect with external data sources and tools. It uses JSON-RPC 2.0 for communication and supports: - Custom tools/functions - Resource access - Authentication (OAuth 2.0) - Real-time capabilities - Persistent connections ## What's New in Developer Mode ### September 2025 Launch Features The September 9th, 2025 announcement introduced groundbreaking capabilities: #### Core Features - **MCP Server Integration**: Direct connection to custom MCP servers - **Tool Execution**: ChatGPT can now execute custom tools on your servers - **OAuth 2.0 Support**: Production-ready authentication and authorization - **Dynamic Tool Discovery**: Automatic discovery and registration of available tools - **Persistent Sessions**: Maintain state and context across conversations - **Real-time Interaction**: Bi-directional communication with external services #### Significant New Capabilities 1. **Custom Tool Creation**: Define any function or API as a tool - Database queries - File system operations - API integrations - Custom business logic 2. **Authentication & Security**: - OAuth 2.0 Authorization Code Flow with PKCE - Dynamic client registration - Secure token management - User-specific data access 3. **Resource Management**: - Access to files, databases, and APIs - Resource permission control - Rate limiting and quotas 4. **State Persistence**: - Session management across conversations - User context preservation - Transaction support 5. **Enterprise Integration**: - Connect to internal tools and databases - Custom authentication providers - Compliance and audit logging ## How to Enable Developer Mode ### Accessing Developer Mode in ChatGPT Since the September 9th, 2025 launch, Developer Mode is available to all ChatGPT users: 1. **Open ChatGPT Settings**: - Navigate to https://chatgpt.com - Click on your profile icon - Select "Settings" 2. **Navigate to Connectors**: - Go to "Connectors" in the settings menu - Select "Advanced Settings" - Enable "Developer Mode (beta)" 3. **Create a New Connector**: - Click "Create" next to "Browser connectors" - You'll see the "New Connector" modal ### Configuring Your MCP Server In the New Connector modal, fill in the following: 1. **Basic Information**: - **Icon** (optional): Upload a 128x128px icon - **Name**: Enter a name (e.g., "Custom MCP Tool") - **Description** (optional): Brief explanation of what it does 2. **Server Configuration**: - **MCP Server URL**: `https://mcp-oauth-server.fly.dev` (or your server URL) - **Authentication**: Select "OAuth" 3. **Security Notice**: - You'll see: "Beta intended for developer use only" - Check "I trust this application" - Note: "Custom connectors are not verified by OpenAI. Malicious developers may attempt to steal your data." 4. **Authorization** (One-time setup): - Click "Create" or "Connect" - You'll be redirected to the login/register page - Sign in or create a new account - Grant permissions to ChatGPT - You'll be redirected back to ChatGPT 5. **Verify Tools**: - After authorization, the modal will show available "Actions" - You should see tools like: search, fetch, write, analyze, etc. ### Using Your MCP Server To use your connected MCP server: 1. **Start a New Chat**: - Click the "+" button to create a new chat 2. **Select Developer Mode**: - Click "... More >" in the model selector - Choose "Developer Mode" - Select the name of the MCP connector you added 3. **Test the Connection**: - Try a command like "search for test data" - The MCP tools will be available throughout the conversation ## Requirements & Limitations ### ⚠️ Critical Requirements 1. **Required Tools**: ChatGPT validates that your MCP server implements: - `search` - MUST be present for information retrieval - `fetch` - MUST be present for data fetching by ID Without these, ChatGPT will show "search action not found" error. 2. **OAuth Discovery**: Must be at root domain - ✅ `https://your-domain.com/.well-known/oauth-authorization-server` - ❌ `https://your-domain.com/functions/v1/.well-known/oauth-authorization-server` 3. **MCP Endpoint**: Must be at root path `/` - ChatGPT sends POST requests to the base URL 4. **Client ID Format**: ChatGPT uses UUID-format client IDs - Example: `47bc35b3-0d0f-4cbf-905f-d68125105b24` - Your server must accept any valid UUID, not just hardcoded values ### Platform Limitations - **Supabase Edge Functions**: ❌ Won't work for OAuth (served under `/functions/v1/`) - **Vercel Functions**: ⚠️ Can work but requires root path configuration - **Fly.io**: ✅ Full control over root domain (recommended) - **Railway/Render**: ✅ Also work well ## Option 1: Non-OAuth Implementation For development or internal tools that don't require authentication. ### Complete Server Code Create `server.js`: ```javascript import express from 'express'; import cors from 'cors'; const app = express(); const PORT = process.env.PORT || 8080; // Configuration const PROTOCOL_VERSION = "2025-06-18"; const SERVER_NAME = "mcp-demo"; const SERVER_VERSION = "1.0.0"; // Middleware const ALLOWED_ORIGINS = [ 'https://chatgpt.com', 'https://chat.openai.com', 'https://platform.openai.com' ]; const DEFAULT_ALLOWED_HEADERS = [ 'Content-Type', 'Authorization', 'MCP-Protocol-Version', 'MCP-Client-ID', 'MCP-Connection-ID', 'MCP-Sequence-ID', 'MCP-Session-ID', 'MCP-Tenant-ID', 'OpenAI-Beta' ]; const ALLOWED_METHODS = ['GET', 'POST', 'OPTIONS']; app.use((req, res, next) => { if (req.method !== 'OPTIONS') { return next(); } const origin = req.headers.origin; if (!origin || !ALLOWED_ORIGINS.includes(origin)) { return res.sendStatus(403); } const requestHeaders = req.headers['access-control-request-headers']; res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', ALLOWED_METHODS.join(',')); res.header( 'Access-Control-Allow-Headers', requestHeaders || DEFAULT_ALLOWED_HEADERS.join(',') ); res.header('Vary', 'Origin'); res.sendStatus(204); }); app.use(cors({ origin: ALLOWED_ORIGINS, credentials: true, allowedHeaders: DEFAULT_ALLOWED_HEADERS, methods: ALLOWED_METHODS, optionsSuccessStatus: 204 })); app.use(express.json()); // MCP Protocol Handler app.post('/', async (req, res) => { const { jsonrpc, method, params = {}, id } = req.body; switch (method) { case 'initialize': res.json({ jsonrpc: '2.0', id, result: { protocolVersion: PROTOCOL_VERSION, capabilities: { logging: {}, prompts: { listChanged: false }, resources: { listChanged: false }, tools: { listChanged: false } }, serverInfo: { name: SERVER_NAME, title: 'MCP Demo Server', version: SERVER_VERSION } } }); break; case 'initialized': res.status(200).end(); break; case 'tools/list': res.json({ jsonrpc: '2.0', id, result: { tools: [ { name: 'search', title: 'Search', description: 'Search for information', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'], additionalProperties: false } }, { name: 'fetch', title: 'Fetch Data', description: 'Fetch data by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Resource ID' } }, required: ['id'], additionalProperties: false } }, { name: 'calculate', title: 'Calculate', description: 'Perform calculations', inputSchema: { type: 'object', properties: { expression: { type: 'string', description: 'Math expression' } }, required: ['expression'], additionalProperties: false } }, { name: 'binance_klines', title: 'Binance K-Line Data', description: 'Fetch candlestick data from Binance', inputSchema: { type: 'object', properties: { symbol: { type: 'string', description: 'Trading pair symbol, e.g., BTCUSDT' }, interval: { type: 'string', description: 'Binance K-line interval', default: '1h' }, limit: { type: 'integer', minimum: 1, maximum: 1000, description: 'Number of candles to fetch (max 1000)', default: 100 } }, required: ['symbol'], additionalProperties: false } } ] } }); break; case 'tools/call': const { name, arguments: args = {} } = params; let content; switch (name) { case 'search': // Mock search results content = [{ type: 'text', text: JSON.stringify([ { id: '1', title: 'Result 1', content: `Found: ${args.query}` }, { id: '2', title: 'Result 2', content: 'Additional result' } ]) }]; break; case 'fetch': // Mock fetch result content = [{ type: 'text', text: JSON.stringify({ id: args.id, title: `Resource ${args.id}`, content: `Data for ${args.id}`, timestamp: new Date().toISOString() }) }]; break; case 'calculate': // Simple calculator try { // WARNING: eval is dangerous! Use a proper math library in production const result = eval(args.expression.replace(/[^0-9+\-*/().\s]/g, '')); content = [{ type: 'text', text: `${args.expression} = ${result}` }]; } catch (error) { content = [{ type: 'text', text: `Error: Invalid expression` }]; } break; case 'binance_klines': { const symbol = String(args.symbol || '').toUpperCase().replace(/[^A-Z0-9]/g, ''); const interval = String(args.interval || '1h'); const limit = Math.min(Math.max(parseInt(args.limit ?? 100, 10) || 100, 1), 1000); const allowedIntervals = new Set([ '1m','3m','5m','15m','30m', '1h','2h','4h','6h','8h','12h', '1d','3d','1w','1M' ]); if (!symbol) { content = [{ type: 'text', text: 'Error: symbol is required' }]; break; } if (!allowedIntervals.has(interval)) { content = [{ type: 'text', text: `Error: interval must be one of ${Array.from(allowedIntervals).join(', ')}` }]; break; } try { const url = new URL('https://api.binance.com/api/v3/klines'); url.searchParams.set('symbol', symbol); url.searchParams.set('interval', interval); url.searchParams.set('limit', String(limit)); const response = await fetch(url, { headers: { 'Accept': 'application/json', 'User-Agent': 'crypto-mcp-demo' } }); if (!response.ok) { content = [{ type: 'text', text: `Error: Binance API responded with ${response.status}` }]; break; } const data = await response.json(); content = [{ type: 'text', text: JSON.stringify({ symbol, interval, limit, candles: data.map(([openTime, open, high, low, close, volume, closeTime]) => ({ openTime, open, high, low, close, volume, closeTime })) }) }]; } catch (error) { content = [{ type: 'text', text: `Error: Failed to fetch Binance data (${error.message})` }]; } } break; default: return res.json({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } }); } res.json({ jsonrpc: '2.0', id, result: { content, isError: false } }); break; case 'resources/list': res.json({ jsonrpc: '2.0', id, result: { resources: [], nextCursor: null } }); break; default: res.json({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }); } }); // Health check endpoint app.get('/', (req, res) => { res.json({ name: SERVER_NAME, version: SERVER_VERSION, protocol: PROTOCOL_VERSION, status: 'running' }); }); app.listen(PORT, () => { console.log(`🚀 MCP Server running on port ${PORT}`); }); ``` ### Package.json ```json { "name": "mcp-demo-server", "version": "1.0.0", "type": "module", "scripts": { "start": "node server.js", "dev": "node --watch server.js" }, "dependencies": { "express": "^4.18.2", "cors": "^2.8.5" } } ``` ## Option 2: OAuth Implementation with Supabase For production services requiring authentication. ### Complete OAuth Server Code Create `server-oauth.js`: ```javascript import express from 'express'; import cors from 'cors'; import { randomUUID } from 'crypto'; import { createClient } from '@supabase/supabase-js'; const app = express(); const PORT = process.env.PORT || 8080; // Configuration const PROTOCOL_VERSION = "2025-06-18"; const SERVER_NAME = "mcp-oauth"; const SERVER_VERSION = "2.4.0"; // Supabase configuration const SUPABASE_URL = process.env.SUPABASE_URL || 'https://your-project.supabase.co'; const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || 'your-anon-key'; const supabaseAnon = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); // Get base URL const BASE_URL = process.env.BASE_URL || `https://${process.env.FLY_APP_NAME}.fly.dev` || 'http://localhost:8080'; // Middleware const ALLOWED_ORIGINS = [ 'https://chatgpt.com', 'https://chat.openai.com', 'https://platform.openai.com', 'http://localhost:3000' ]; const DEFAULT_ALLOWED_HEADERS = [ 'Content-Type', 'Authorization', 'MCP-Protocol-Version', 'MCP-Client-ID', 'MCP-Connection-ID', 'MCP-Sequence-ID', 'MCP-Session-ID', 'MCP-Tenant-ID', 'OpenAI-Beta' ]; const ALLOWED_METHODS = ['GET', 'POST', 'OPTIONS']; app.use((req, res, next) => { if (req.method !== 'OPTIONS') { return next(); } const origin = req.headers.origin; if (!origin || !ALLOWED_ORIGINS.includes(origin)) { return res.sendStatus(403); } const requestHeaders = req.headers['access-control-request-headers']; res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', ALLOWED_METHODS.join(',')); res.header( 'Access-Control-Allow-Headers', requestHeaders || DEFAULT_ALLOWED_HEADERS.join(',') ); res.header('Vary', 'Origin'); res.sendStatus(204); }); app.use(cors({ origin: ALLOWED_ORIGINS, credentials: true, allowedHeaders: DEFAULT_ALLOWED_HEADERS, methods: ALLOWED_METHODS, optionsSuccessStatus: 204 })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // OAuth Storage (use Redis/DB in production) const clients = new Map(); const authCodes = new Map(); const tokens = new Map(); const refreshTokens = new Map(); const users = new Map(); // Helper: Identify ChatGPT clients function isChatGPTClient(clientId) { return clientId === 'chatgpt' || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(clientId); } // OAuth Discovery Endpoint (MUST be at root domain) app.get('/.well-known/oauth-authorization-server', (req, res) => { res.json({ issuer: BASE_URL, authorization_endpoint: `${BASE_URL}/authorize`, token_endpoint: `${BASE_URL}/token`, registration_endpoint: `${BASE_URL}/register`, response_types_supported: ['code'], response_modes_supported: ['query'], grant_types_supported: ['authorization_code', 'refresh_token'], code_challenge_methods_supported: ['S256', 'plain'], scopes_supported: ['mcp:read', 'mcp:write'], token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic', 'none'], service_documentation: 'https://modelcontextprotocol.io', ui_locales_supported: ['en'], claims_supported: ['sub', 'email', 'name'], request_uri_parameter_supported: false, require_request_uri_registration: false, request_parameter_supported: false, authorization_response_iss_parameter_supported: true }); }); // OAuth Authorization Endpoint - GET (show login form) app.get('/authorize', (req, res) => { const { client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method } = req.query; // Auto-register ChatGPT clients if (isChatGPTClient(client_id) && !clients.has(client_id)) { clients.set(client_id, { client_id: client_id, client_secret: null, redirect_uris: [ 'https://chatgpt.com/oauth/callback', 'https://chat.openai.com/oauth/callback', 'https://chatgpt.com/connector_platform_oauth_redirect', 'http://localhost:3000/callback' ], grant_types: ['authorization_code', 'refresh_token'], token_endpoint_auth_method: 'none' }); } const client = clients.get(client_id); if (!client) { return res.status(400).send('Invalid client_id'); } // Validate redirect URI const validRedirect = client.redirect_uris.some(uri => redirect_uri === uri || redirect_uri?.startsWith(uri.replace(/\/callback$/, '')) ); if (!validRedirect) { return res.status(400).send('Invalid redirect_uri'); } // Show login form res.send(` <!DOCTYPE html> <html> <head> <title>Sign In - MCP Server</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .container { background: white; border-radius: 20px; padding: 40px; box-shadow: 0 20px 60px rgba(0,0,0,0.15); width: 100%; max-width: 400px; } h1 { color: #1a202c; font-size: 28px; margin-bottom: 8px; font-weight: 700; } .subtitle { color: #718096; margin-bottom: 32px; font-size: 14px; } .form-group { margin-bottom: 24px; } label { display: block; color: #4a5568; font-size: 14px; font-weight: 600; margin-bottom: 8px; } input { width: 100%; padding: 12px 16px; border: 2px solid #e2e8f0; border-radius: 10px; font-size: 16px; transition: all 0.3s; background: #f7fafc; } input:focus { outline: none; border-color: #667eea; background: white; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } button { width: 100%; padding: 14px; margin: 8px 0; border: none; border-radius: 10px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s; } .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); } .btn-secondary { background: white; color: #667eea; border: 2px solid #667eea; } .btn-secondary:hover { background: #f7fafc; } .error { background: #fed7d7; color: #c53030; padding: 12px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; } .demo-note { background: #edf2f7; padding: 16px; border-radius: 10px; margin-top: 24px; font-size: 13px; color: #4a5568; border-left: 4px solid #667eea; } .demo-note strong { color: #2d3748; } .app-info { background: #f7fafc; padding: 16px; border-radius: 10px; margin-bottom: 24px; border: 1px solid #e2e8f0; } .app-name { color: #667eea; font-weight: 700; font-size: 16px; } .scope-list { margin-top: 8px; padding-left: 20px; } .scope-item { color: #4a5568; font-size: 14px; margin: 4px 0; } .divider { height: 1px; background: #e2e8f0; margin: 24px 0; } .signup-section { text-align: center; margin-top: 20px; } .signup-text { color: #718096; font-size: 14px; margin-bottom: 12px; } .btn-text { background: transparent; color: #667eea; border: none; text-decoration: underline; padding: 8px; cursor: pointer; } .btn-text:hover { color: #764ba2; } </style> </head> <body> <div class="container"> <h1>Sign In</h1> <div class="subtitle">Connect your account to ChatGPT</div> <div class="app-info"> <div class="app-name">ChatGPT MCP Integration</div> <div class="scope-list"> <div class="scope-item">• Read access to MCP tools</div> <div class="scope-item">• Execute MCP functions</div> </div> </div> <form method="post" action="/authorize"> <input type="hidden" name="client_id" value="${client_id}"> <input type="hidden" name="redirect_uri" value="${redirect_uri}"> <input type="hidden" name="response_type" value="${response_type}"> <input type="hidden" name="scope" value="${scope || 'mcp:read mcp:write'}"> <input type="hidden" name="state" value="${state || ''}"> <input type="hidden" name="code_challenge" value="${code_challenge || ''}"> <input type="hidden" name="code_challenge_method" value="${code_challenge_method || ''}"> <div class="form-group"> <label for="email">Email</label> <input type="email" id="email" name="email" required placeholder="you@example.com"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required placeholder="••••••••"> </div> <button type="submit" name="action" value="approve" class="btn-primary"> Sign In & Authorize </button> <div class="divider"></div> <button type="submit" name="action" value="demo" class="btn-secondary"> Use Demo Account </button> </form> <div class="signup-section"> <div class="signup-text">Don't have an account?</div> <button class="btn-text" onclick="window.location.href='/signup?${new URLSearchParams(req.query).toString()}'"> Create New Account </button> </div> <div class="demo-note"> <strong>Demo Mode:</strong> Click "Use Demo Account" to test without creating an account. For production use, create an account or sign in with your credentials. </div> </div> </body> </html> `); }); // OAuth Authorization Endpoint - POST (process login) app.post('/authorize', async (req, res) => { const { client_id, redirect_uri, response_type, scope, state, action, email, password, code_challenge, code_challenge_method } = req.body; if (action === 'deny') { const redirectUrl = new URL(redirect_uri); redirectUrl.searchParams.set('error', 'access_denied'); if (state) redirectUrl.searchParams.set('state', state); return res.redirect(redirectUrl.toString()); } let user; if (action === 'demo') { // Demo mode user = { id: 'demo-user', email: 'demo@example.com', isDemo: true }; } else { // Try Supabase authentication try { const { data, error } = await supabaseAnon.auth.signInWithPassword({ email, password }); if (error) { // Show error page return res.send(` <!DOCTYPE html> <html> <head> <title>Sign In Error</title> <style> body { font-family: system-ui; padding: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } .container { max-width: 400px; margin: 0 auto; background: white; padding: 40px; border-radius: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.15); } .error { background: #fed7d7; color: #c53030; padding: 12px; border-radius: 8px; margin-bottom: 20px; } button { background: #667eea; color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; } </style> </head> <body> <div class="container"> <h1>Authentication Failed</h1> <div class="error">Invalid email or password. Please try again.</div> <button onclick="history.back()">Go Back</button> </div> </body> </html> `); } user = { id: data.user.id, email: data.user.email, supabase_session: data.session }; // Store user session users.set(user.id, user); } catch (err) { console.error('Supabase auth error:', err); // Fall back to demo mode on error user = { id: 'demo-user', email: 'demo@example.com', isDemo: true }; } } // Generate authorization code const code = randomUUID(); // Store auth code with PKCE if provided authCodes.set(code, { client_id, redirect_uri, scope: scope || 'mcp:read mcp:write', code_challenge, code_challenge_method, expires_at: Date.now() + 600000, // 10 minutes user: user }); // Redirect with code const redirectUrl = new URL(redirect_uri); redirectUrl.searchParams.set('code', code); if (state) redirectUrl.searchParams.set('state', state); res.redirect(redirectUrl.toString()); }); // OAuth Token Endpoint app.post('/token', (req, res) => { const { grant_type, code, redirect_uri, client_id, client_secret, refresh_token, code_verifier } = req.body; if (grant_type === 'authorization_code') { const authCode = authCodes.get(code); if (!authCode || authCode.expires_at < Date.now()) { return res.status(400).json({ error: 'invalid_grant' }); } // Validate client if (authCode.client_id !== client_id || authCode.redirect_uri !== redirect_uri) { return res.status(400).json({ error: 'invalid_request' }); } // Generate tokens const accessToken = randomUUID(); const newRefreshToken = randomUUID(); // Store tokens tokens.set(accessToken, { client_id, user: authCode.user, scope: authCode.scope, expires_at: Date.now() + 3600000 // 1 hour }); refreshTokens.set(newRefreshToken, { client_id, user: authCode.user, scope: authCode.scope, expires_at: Date.now() + 1209600000 // 14 days }); // Delete used code authCodes.delete(code); // Return tokens res.json({ access_token: accessToken, token_type: 'Bearer', expires_in: 3600, refresh_token: newRefreshToken, scope: authCode.scope }); } else if (grant_type === 'refresh_token') { const tokenData = refreshTokens.get(refresh_token); if (!tokenData || tokenData.expires_at < Date.now()) { return res.status(400).json({ error: 'invalid_grant' }); } // Generate new access token const accessToken = randomUUID(); tokens.set(accessToken, { client_id: tokenData.client_id, user: tokenData.user, scope: tokenData.scope, expires_at: Date.now() + 3600000 }); res.json({ access_token: accessToken, token_type: 'Bearer', expires_in: 3600, scope: tokenData.scope }); } else { res.status(400).json({ error: 'unsupported_grant_type' }); } }); // Dynamic Client Registration app.post('/register', (req, res) => { const { redirect_uris, grant_types, response_types, client_name, scope, token_endpoint_auth_method } = req.body; const clientId = randomUUID(); const clientSecret = randomUUID(); const client = { client_id: clientId, client_secret: token_endpoint_auth_method === 'none' ? undefined : clientSecret, redirect_uris: redirect_uris || [], grant_types: grant_types || ['authorization_code'], response_types: response_types || ['code'], scope: scope || 'mcp:read mcp:write', token_endpoint_auth_method: token_endpoint_auth_method || 'client_secret_basic', client_name: client_name || 'MCP Client' }; clients.set(clientId, client); res.status(201).json({ client_id: clientId, client_secret: client.client_secret, client_id_issued_at: Math.floor(Date.now() / 1000), grant_types: client.grant_types, redirect_uris: client.redirect_uris, response_types: client.response_types, scope: client.scope, token_endpoint_auth_method: client.token_endpoint_auth_method, client_name: client.client_name }); }); // Token validation helper function validateToken(authHeader) { if (!authHeader || !authHeader.startsWith('Bearer ')) { return null; } const token = authHeader.substring(7); const tokenData = tokens.get(token); if (!tokenData || tokenData.expires_at < Date.now()) { return null; } return tokenData; } // MCP Protocol Handler (MUST be at root path) app.post('/', async (req, res) => { const { jsonrpc, method, params = {}, id } = req.body; // Check authentication for protected methods const tokenData = validateToken(req.headers.authorization); const requiresAuth = method !== 'initialize' && method !== 'initialized'; if (requiresAuth && !tokenData) { return res.json({ jsonrpc: '2.0', id, error: { code: -32000, message: 'Authentication required', data: { oauth_authorize_url: `${BASE_URL}/authorize?client_id=chatgpt&response_type=code&scope=mcp:read+mcp:write` } } }); } switch (method) { case 'initialize': res.json({ jsonrpc: '2.0', id, result: { protocolVersion: PROTOCOL_VERSION, capabilities: { logging: {}, prompts: { listChanged: false }, resources: { listChanged: false }, tools: { listChanged: false } }, serverInfo: { name: SERVER_NAME, title: 'MCP OAuth Server', version: SERVER_VERSION } } }); break; case 'initialized': res.status(200).end(); break; case 'tools/list': res.json({ jsonrpc: '2.0', id, result: { tools: [ // REQUIRED TOOLS { name: 'search', title: 'Search', description: 'Search for information', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'], additionalProperties: false } }, { name: 'fetch', title: 'Fetch Data', description: 'Fetch data by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Resource ID' } }, required: ['id'], additionalProperties: false } }, // Additional tools { name: 'analyze', title: 'Analyze Data', description: 'Analyze data and provide insights', inputSchema: { type: 'object', properties: { data: { type: 'string', description: 'Data to analyze' }, type: { type: 'string', enum: ['sentiment', 'summary', 'statistics'], description: 'Type of analysis' } }, required: ['data', 'type'], additionalProperties: false } }, { name: 'generate', title: 'Generate Content', description: 'Generate various types of content', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['text', 'code', 'json'], description: 'Content type' }, prompt: { type: 'string', description: 'Generation prompt' } }, required: ['type', 'prompt'], additionalProperties: false } } ] } }); break; case 'tools/call': const { name, arguments: args = {} } = params; let content; switch (name) { case 'search': content = [{ type: 'text', text: JSON.stringify([ { id: '1', title: 'Search Result 1', content: `Found: ${args.query}`, user: tokenData.user.email }, { id: '2', title: 'Search Result 2', content: 'Additional result', authenticated: true } ]) }]; break; case 'fetch': content = [{ type: 'text', text: JSON.stringify({ id: args.id, title: `Resource ${args.id}`, content: `Data for ${args.id}`, authenticated: true, user: tokenData.user.email, timestamp: new Date().toISOString() }) }]; break; case 'analyze': content = [{ type: 'text', text: JSON.stringify({ input: args.data, analysis_type: args.type, result: `Analysis of type ${args.type} completed`, user: tokenData.user.email }) }]; break; case 'generate': content = [{ type: 'text', text: JSON.stringify({ type: args.type, generated: `Generated ${args.type} content based on: ${args.prompt}`, user: tokenData.user.email }) }]; break; default: return res.json({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } }); } res.json({ jsonrpc: '2.0', id, result: { content, isError: false } }); break; case 'resources/list': res.json({ jsonrpc: '2.0', id, result: { resources: [], nextCursor: null } }); break; default: res.json({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }); } }); // Health check app.get('/', (req, res) => { res.json({ name: SERVER_NAME, version: SERVER_VERSION, protocol: PROTOCOL_VERSION, endpoints: { mcp: 'POST /', oauth_discovery: '/.well-known/oauth-authorization-server', authorize: '/authorize', token: '/token', register: '/register' }, status: 'running' }); }); app.listen(PORT, () => { console.log(`🚀 MCP OAuth Server v${SERVER_VERSION} running on port ${PORT}`); console.log(`📝 Protocol: MCP ${PROTOCOL_VERSION}`); console.log(`🔐 OAuth discovery: ${BASE_URL}/.well-known/oauth-authorization-server`); console.log(`🌐 Base URL: ${BASE_URL}`); console.log(`✅ Ready for ChatGPT integration!`); }); ``` ### Package.json for OAuth ```json { "name": "mcp-oauth-server", "version": "2.4.0", "type": "module", "scripts": { "start": "node server-oauth.js", "dev": "node --watch server-oauth.js" }, "dependencies": { "express": "^4.18.2", "cors": "^2.8.5", "@supabase/supabase-js": "^2.39.1" } } ``` ## Deployment Options This MCP server can be deployed to multiple cloud platforms. Each platform has different features, pricing, and deployment approaches: ### Platform Comparison | Platform | Free Tier | Always-On | Setup Complexity | Best For | |----------|-----------|-----------|------------------|----------| | **Railway** | No | Yes ($5/month) | Easy | Production, CI/CD | | **Render** | Yes* | Paid plans only | Easy | Testing, Development | | **Fly.io** | Yes | Paid scaling | Medium | Advanced users | *Render free tier has cold starts and limitations ### Quick Deploy Links - [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/mcp-demo-server) - [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/your-username/crypto-mcp) ### Deployment Guides #### Railway Deployment Railway offers the simplest deployment experience with excellent GitHub integration. **Quick Setup:** 1. Connect your GitHub repository at [railway.app](https://railway.app) 2. Railway auto-detects Node.js and uses `railway.json` configuration 3. Deploys automatically on every push **Configuration:** See [RAILWAY.md](./RAILWAY.md) for detailed setup instructions. **Environment Variables (OAuth only):** ```bash SUPABASE_URL=https://your-project.supabase.co SUPABASE_ANON_KEY=your-anon-key SUPABASE_SERVICE_ROLE_KEY=your-service-role-key BASE_URL=https://your-app.railway.app ``` **Pricing:** $5/month per service (always-on, no cold starts) --- #### Render Deployment Render provides a free tier option and excellent documentation. **Quick Setup:** 1. Connect your GitHub repository at [render.com](https://render.com) 2. Choose "Web Service" and configure: - Build Command: `npm install` - Start Command: `npm start` 3. Optional: Use `render.yaml` for advanced configuration **Configuration:** See [RENDER.md](./RENDER.md) for detailed setup instructions. **Free Tier Notes:** - Service sleeps after 15 minutes of inactivity - ~30 second cold start time - 750 hours/month limit - Upgrade to Starter ($7/month) for always-on service --- #### Fly.io Deployment (Legacy) ### Prerequisites 1. Install Fly CLI: ```bash curl -L https://fly.io/install.sh | sh ``` 2. Sign up and login: ```bash flyctl auth signup # or flyctl auth login ``` ### Dockerfile Create `Dockerfile`: ```dockerfile FROM node:20-slim WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm ci --only=production # Copy application files COPY . . # Expose port EXPOSE 8080 # Start the application CMD ["npm", "start"] ``` ### fly.toml Configuration Create `fly.toml`: ```toml app = 'mcp-oauth-server' primary_region = 'iad' [build] [http_service] internal_port = 8080 force_https = true auto_stop_machines = false auto_start_machines = true min_machines_running = 1 processes = ['app'] [[vm]] memory = '1gb' cpu_kind = 'shared' cpus = 1 [env] PORT = "8080" NODE_ENV = "production" ``` ### Deploy Script Create `deploy.sh`: ```bash #!/bin/bash echo "🚀 Deploying MCP OAuth Server to Fly.io" # Check if app exists if ! flyctl status >/dev/null 2>&1; then echo "📝 Creating new Fly.io app..." flyctl launch --name mcp-oauth-server --region iad --yes else echo "✅ App already exists" fi # Set secrets (if using Supabase) echo "🔐 Setting environment variables..." flyctl secrets set \ SUPABASE_URL="https://your-project.supabase.co" \ SUPABASE_ANON_KEY="your-anon-key" # Deploy echo "🚢 Deploying application..." flyctl deploy --ha=false # Show status echo "✨ Deployment complete!" flyctl status flyctl info echo "🌐 Your MCP server is available at:" flyctl info | grep "Hostname" echo "" echo "📝 OAuth Discovery URL:" echo "https://$(flyctl info -j | jq -r .Hostname)/.well-known/oauth-authorization-server" ``` Make it executable: ```bash chmod +x deploy.sh ``` ### Deploy Your Server ```bash # First time setup flyctl launch --name your-mcp-server --region iad # Deploy flyctl deploy --ha=false # Check status flyctl status # View logs flyctl logs ``` ## Testing Your MCP Server ### 1. Test Locally ```bash # Install dependencies npm install # Run locally npm start # Test MCP endpoint curl -X POST http://localhost:8080/ \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}' # Test OAuth discovery curl http://localhost:8080/.well-known/oauth-authorization-server ``` ### 2. Connect to ChatGPT 1. Go to ChatGPT Settings → Developer 2. Click "Add MCP Server" 3. Enter your server URL: `https://your-app.fly.dev` 4. ChatGPT will: - Discover OAuth endpoints automatically - Prompt you to authorize - Show available tools after authorization ### 3. Test Tools After connecting, try these prompts in ChatGPT: - "Search for information about AI" - "Fetch data with ID 123" - "Analyze this text for sentiment" - "Generate a JSON schema" - "获取 BTCUSDT 1 小时 K 线数据" ## Common Gotchas & Solutions ### 1. "Only ChatGPT client is supported" Error **Problem**: Server rejects ChatGPT's UUID client_id **Solution**: Accept any UUID format: ```javascript function isChatGPTClient(clientId) { return clientId === 'chatgpt' || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(clientId); } ``` ### 2. "search action not found" Error **Problem**: ChatGPT requires `search` and `fetch` tools **Solution**: Always include these tools in `tools/list`: ```javascript { name: 'search', title: 'Search', description: 'Search for information', inputSchema: { /* ... */ } }, { name: 'fetch', title: 'Fetch Data', description: 'Fetch data by ID', inputSchema: { /* ... */ } } ``` ### 3. OAuth Discovery Fails **Problem**: Supabase Edge Functions serve from `/functions/v1/` **Solution**: Use Fly.io or another platform with root domain control ### 4. Redirect URI Mismatch **Problem**: ChatGPT uses different redirect URIs **Solution**: Include all variants: ```javascript redirect_uris: [ 'https://chatgpt.com/oauth/callback', 'https://chat.openai.com/oauth/callback', 'https://chatgpt.com/connector_platform_oauth_redirect' ] ``` ### 5. CORS Issues **Problem**: Browser blocks requests **Solution**: Configure CORS properly: ```javascript const ALLOWED_ORIGINS = [ 'https://chatgpt.com', 'https://chat.openai.com', 'https://platform.openai.com' ]; const DEFAULT_ALLOWED_HEADERS = [ 'Content-Type', 'Authorization', 'MCP-Protocol-Version', 'MCP-Client-ID', 'MCP-Connection-ID', 'MCP-Sequence-ID', 'MCP-Session-ID', 'MCP-Tenant-ID', 'OpenAI-Beta' ]; const ALLOWED_METHODS = ['GET', 'POST', 'OPTIONS']; app.use((req, res, next) => { if (req.method !== 'OPTIONS') { return next(); } const origin = req.headers.origin; if (!origin || !ALLOWED_ORIGINS.includes(origin)) { return res.sendStatus(403); } const requestHeaders = req.headers['access-control-request-headers']; res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', ALLOWED_METHODS.join(',')); res.header( 'Access-Control-Allow-Headers', requestHeaders || DEFAULT_ALLOWED_HEADERS.join(',') ); res.header('Vary', 'Origin'); res.sendStatus(204); }); app.use(cors({ origin: ALLOWED_ORIGINS, credentials: true, allowedHeaders: DEFAULT_ALLOWED_HEADERS, methods: ALLOWED_METHODS, optionsSuccessStatus: 204 })); ``` ## Troubleshooting ### Check Server Logs ```bash # Fly.io logs flyctl logs --tail # Local logs npm run dev ``` ### Common Error Messages | Error | Cause | Solution | |-------|-------|----------| | "Method not found" | Unimplemented MCP method | Add method to switch statement | | "Invalid client_id" | Client not registered | Auto-register ChatGPT clients | | "Authentication required" | Missing/invalid token | Implement OAuth flow | | "Invalid redirect_uri" | URI not in whitelist | Add URI to client configuration | ### Debug OAuth Flow 1. Check discovery endpoint: ```bash curl https://your-app.fly.dev/.well-known/oauth-authorization-server ``` 2. Test authorization: ```bash # Open in browser https://your-app.fly.dev/authorize?client_id=chatgpt&response_type=code ``` 3. Monitor token exchange: ```javascript // Add logging in token endpoint console.log('Token request:', { grant_type, client_id }); ``` ## Security Best Practices 1. **Use Environment Variables**: Never hardcode secrets 2. **Validate Inputs**: Sanitize all user inputs 3. **Rate Limiting**: Add rate limiting in production 4. **HTTPS Only**: Always use HTTPS in production 5. **Token Expiration**: Set appropriate token lifetimes 6. **Audit Logging**: Log all authentication events ## Advanced Features ### Add Custom Tools ```javascript { name: 'database_query', title: 'Database Query', description: 'Execute database queries', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'SQL query' }, database: { type: 'string', description: 'Database name' } }, required: ['query'], additionalProperties: false } } ``` ### Add Resources ```javascript case 'resources/list': res.json({ jsonrpc: '2.0', id, result: { resources: [ { uri: 'resource://database/users', name: 'User Database', description: 'Access to user data', mimeType: 'application/json' } ], nextCursor: null } }); break; ``` ### Add Prompts ```javascript case 'prompts/list': res.json({ jsonrpc: '2.0', id, result: { prompts: [ { name: 'analyze_code', description: 'Analyze code quality', arguments: [ { name: 'code', description: 'Code to analyze', required: true } ] } ] } }); break; ``` ## Real-World Use Cases Since the September 9th announcement, developers have built MCP servers for: ### Business Applications - **CRM Integration**: Connect ChatGPT to Salesforce, HubSpot, or custom CRMs - **Database Queries**: Direct SQL access to production databases - **Analytics Tools**: Connect to Google Analytics, Mixpanel, or custom dashboards - **Document Management**: Search and retrieve from SharePoint, Confluence, or wikis ### Developer Tools - **Code Repository Access**: Browse and search GitHub/GitLab repositories - **CI/CD Integration**: Trigger builds, check deployment status - **API Testing**: Execute and test API endpoints directly from ChatGPT - **Log Analysis**: Search and analyze application logs in real-time ### Personal Productivity - **Note Taking**: Connect to Notion, Obsidian, or personal knowledge bases - **Task Management**: Integrate with Jira, Asana, or todo apps - **Email Access**: Search and compose emails (with proper authentication) - **Calendar Integration**: Schedule meetings and check availability ### Community Feedback From the OpenAI Developer Forum and community discussions: > "MCP Server Tools have transformed how we interact with ChatGPT. We can now connect it directly to our internal systems." - Enterprise Developer > "The OAuth support means we can safely expose company tools without security concerns." - Security Engineer > "Building custom tools is straightforward - the hardest part was remembering to include the required 'search' and 'fetch' tools!" - Independent Developer ## Live Example Test the working implementation at: https://mcp-oauth-server.fly.dev 1. **OAuth Discovery**: https://mcp-oauth-server.fly.dev/.well-known/oauth-authorization-server 2. **Registration**: https://mcp-oauth-server.fly.dev/signup (New users can create accounts) 3. **Available Tools**: search, fetch, analyze, generate, and more 4. **Demo Mode**: Sign in with demo credentials or create your own account ## Resources - [MCP Specification](https://modelcontextprotocol.io) - [ChatGPT Developer Settings](https://chatgpt.com/settings/developer) - [OpenAI Community Discussion](https://community.openai.com/t/mcp-server-tools-now-in-chatgpt-developer-mode/1357233) - [Fly.io Documentation](https://fly.io/docs) - [Supabase Auth](https://supabase.com/docs/guides/auth) - [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749) - [Example Implementation](https://github.com/ruvnet/mcp-oauth-server) ## Summary Building an MCP server for ChatGPT requires: 1. **Required tools**: `search` and `fetch` must be implemented 2. **Root domain control**: OAuth discovery at `/.well-known/` 3. **UUID client support**: Accept any UUID-format client_id 4. **Proper CORS**: Configure for ChatGPT domains 5. **Platform choice**: Use Fly.io or similar (not Supabase Edge Functions) Follow this tutorial step-by-step, and you'll have a working MCP server integrated with ChatGPT's new Developer Mode! --- *Last Updated: September 13, 2025* *Version: 2.5.0* *MCP Server Tools Announcement: September 9, 2025* *Live Demo: https://mcp-oauth-server.fly.dev*

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shukehi/crypto-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server