@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
JavaScript
/**
* 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 /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 /shopify-mcp-stdio-client
npx /shopify-mcp-stdio-client --sse
npx /shopify-mcp-stdio-client --url=http://localhost:5000
npx /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);