UNPKG

@pranavchavda/shopify-mcp-stdio-client

Version:

STDIO MCP Client for Shopify e-commerce tools - bridges legacy STDIO clients to modern HTTP/SSE MCP servers

399 lines (334 loc) 10.8 kB
#!/usr/bin/env node /** * STDIO MCP Client Wrapper - Bundled Version * Built: 2025-06-09T15:42:59.999Z * * This is a self-contained version with no external dependencies. * It bridges STDIO-based MCP clients to HTTP/SSE MCP servers. */ /** * STDIO MCP Client Wrapper * * This bridges STDIO-based MCP clients to our HTTP/SSE MCP server. * Usage: npx @pranavchavda/shopify-mcp-stdio-client [--sse] [--url=custom-url] * * Provides compatibility for tools/SDKs that only support STDIO MCP servers * while leveraging our modern HTTP-based implementation. */ import { request } from 'https'; import { request as httpRequest } from 'http'; import { URL } from 'url'; function fetch(url, options = {}) { return new Promise((resolve, reject) => { const parsedUrl = new URL(url); const isHttps = parsedUrl.protocol === 'https:'; const requestFn = isHttps ? request : httpRequest; const reqOptions = { hostname: parsedUrl.hostname, port: parsedUrl.port || (isHttps ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, method: options.method || 'GET', headers: options.headers || {}, timeout: options.timeout || 30000 }; const req = requestFn(reqOptions, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, statusText: res.statusMessage, headers: new Map(Object.entries(res.headers)), text: () => Promise.resolve(data), json: () => Promise.resolve(JSON.parse(data)) }); }); }); req.on('error', reject); req.on('timeout', () => reject(new Error('Request timeout'))); if (options.body) { req.write(options.body); } req.end(); }); } class EventSource { constructor(url, options = {}) { this.url = url; this.readyState = 0; // CONNECTING this.onopen = null; this.onmessage = null; this.onerror = null; this.eventListeners = new Map(); // Since we're primarily using HTTP mode, this is a minimal implementation setTimeout(() => { this.readyState = 1; // OPEN if (this.onopen) this.onopen(); }, 100); } addEventListener(type, listener) { if (!this.eventListeners.has(type)) { this.eventListeners.set(type, []); } this.eventListeners.get(type).push(listener); } close() { this.readyState = 2; // CLOSED } } class STDIOMCPClient { constructor(options = {}) { this.baseUrl = options.url || process.env.MCP_SERVER_URL || 'https://webhook-listener-pranavchavda.replit.app'; this.useSSE = options.sse || false; this.endpoint = this.useSSE ? `${this.baseUrl}/mcp/sse` : `${this.baseUrl}/mcp`; this.bearerToken = options.token || process.env.MCP_BEARER_TOKEN; this.sessionId = null; this.sseConnection = null; this.pendingRequests = new Map(); this.requestId = 1; this.activeRequests = 0; this.setupStdio(); if (this.useSSE) { this.initializeSSE(); } // Send server info to stderr (like real MCP servers) console.error(`STDIO MCP Client bridging to ${this.endpoint}`); console.error(`Mode: ${this.useSSE ? 'Server-Sent Events' : 'HTTP POST'}`); } setupStdio() { process.stdin.setEncoding('utf8'); let buffer = ''; process.stdin.on('data', (chunk) => { buffer += chunk; let lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.trim()) { this.handleStdioRequest(line.trim()); } } }); process.stdin.on('end', () => { if (buffer.trim()) { this.handleStdioRequest(buffer.trim()); } this.checkExit(); }); // Handle process termination process.on('SIGINT', () => this.cleanup()); process.on('SIGTERM', () => this.cleanup()); } initializeSSE() { try { const headers = { 'Accept': 'text/event-stream', 'User-Agent': 'STDIO-MCP-Client/1.0' }; if (this.bearerToken) { headers['Authorization'] = `Bearer ${this.bearerToken}`; } this.sseConnection = new EventSource(this.endpoint, { headers }); this.sseConnection.onopen = () => { console.error('SSE connection established'); }; this.sseConnection.addEventListener('endpoint', (event) => { const data = JSON.parse(event.data); this.sessionEndpoint = data; console.error(`Session endpoint: ${this.sessionEndpoint}`); }); this.sseConnection.addEventListener('message', (event) => { const response = JSON.parse(event.data); this.handleResponse(response); }); this.sseConnection.onerror = (error) => { console.error('SSE connection error:', error); }; } catch (error) { console.error('Failed to initialize SSE:', error); process.exit(1); } } handleStdioRequest(jsonStr) { try { const request = JSON.parse(jsonStr); this.activeRequests++; if (this.useSSE && this.sessionEndpoint) { this.sendSSERequest(request).catch(this.handleError.bind(this)).finally(() => { this.activeRequests--; this.checkExit(); }); } else { this.sendHTTPRequest(request).catch(this.handleError.bind(this)).finally(() => { this.activeRequests--; this.checkExit(); }); } } catch (error) { const errorResponse = { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: error.message } }; process.stdout.write(JSON.stringify(errorResponse) + '\n'); } } checkExit() { // Don't exit if there are pending requests if (this.activeRequests === 0 && process.stdin.readableEnded) { this.cleanup(); } } handleError(error) { const errorResponse = { jsonrpc: "2.0", id: null, error: { code: -32603, message: "Internal error", data: error.message } }; process.stdout.write(JSON.stringify(errorResponse) + '\n'); } async sendHTTPRequest(request) { try { const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'STDIO-MCP-Client/1.0' }; if (this.bearerToken) { headers['Authorization'] = `Bearer ${this.bearerToken}`; } const response = await fetch(this.endpoint, { method: 'POST', headers, body: JSON.stringify(request), timeout: 30000 }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const result = await response.json(); // Send response to stdout (STDIO protocol) process.stdout.write(JSON.stringify(result) + '\n'); } catch (error) { const errorResponse = { jsonrpc: "2.0", id: request.id || null, error: { code: -32603, message: "Transport error", data: error.message } }; process.stdout.write(JSON.stringify(errorResponse) + '\n'); } } async sendSSERequest(request) { try { // Store pending request for response matching this.pendingRequests.set(request.id, request); const headers = { 'Content-Type': 'application/json', 'User-Agent': 'STDIO-MCP-Client/1.0' }; if (this.bearerToken) { headers['Authorization'] = `Bearer ${this.bearerToken}`; } const response = await fetch(this.sessionEndpoint, { method: 'POST', headers, body: JSON.stringify(request) }); if (!response.ok) { const errorResponse = { jsonrpc: "2.0", id: request.id || null, error: { code: -32603, message: `Session error: ${response.status}`, data: await response.text() } }; process.stdout.write(JSON.stringify(errorResponse) + '\n'); this.pendingRequests.delete(request.id); } } catch (error) { const errorResponse = { jsonrpc: "2.0", id: request.id || null, error: { code: -32603, message: "SSE transport error", data: error.message } }; process.stdout.write(JSON.stringify(errorResponse) + '\n'); this.pendingRequests.delete(request.id); } } handleResponse(response) { // Handle SSE response - forward to stdout process.stdout.write(JSON.stringify(response) + '\n'); if (response.id) { this.pendingRequests.delete(response.id); } } cleanup() { if (this.sseConnection) { this.sseConnection.close(); } process.exit(0); } } // Parse command line arguments const args = process.argv.slice(2); const options = {}; for (const arg of args) { if (arg === '--sse') { options.sse = true; } else if (arg.startsWith('--url=')) { options.url = arg.split('=')[1]; } else if (arg.startsWith('--token=')) { options.token = arg.split('=')[1]; } else if (arg === '--help' || arg === '-h') { console.error(` STDIO MCP Client Wrapper for Shopify E-commerce Tools Usage: npx @pranavchavda/shopify-mcp-stdio-client [options] Options: --sse Use Server-Sent Events instead of HTTP POST --url=URL Custom base URL (default: https://webhook-listener-pranavchavda.replit.app) --token=TOKEN Bearer token for authentication (can also use MCP_BEARER_TOKEN env var) --help, -h Show this help message Examples: npx @pranavchavda/shopify-mcp-stdio-client npx @pranavchavda/shopify-mcp-stdio-client --sse npx @pranavchavda/shopify-mcp-stdio-client --url=http://localhost:5000 npx @pranavchavda/shopify-mcp-stdio-client --token=your-bearer-token Environment Variables: MCP_SERVER_URL Default server URL MCP_BEARER_TOKEN Authentication token Available Tools (15+ Shopify e-commerce tools): - Product management and search - Open box listing creation - Pricing and inventory updates - Collection management - GraphQL operations - Tag and metafield operations This tool bridges STDIO-based MCP clients to HTTP/SSE MCP servers, providing compatibility for legacy tools and SDKs. Documentation: https://webhook-listener-pranavchavda.replit.app Repository: https://github.com/pranavchavda/shopify-mcp-server `); process.exit(0); } } // Start the STDIO MCP client new STDIOMCPClient(options);