@sigyl-dev/cli
Version:
Official Sigyl CLI for installing and managing MCP packages. Zero-config installation for public packages, secure API-based authentication.
181 lines (148 loc) • 5.11 kB
text/typescript
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { JSONRPCMessage, JSONRPCError } from "@modelcontextprotocol/sdk/types.js";
interface MCPProxyOptions {
endpoint: string;
apiKey: string;
profile?: string;
}
export class createMCPStdioProxy {
private endpoint: string;
private apiKey: string;
private profile?: string;
private transport: StreamableHTTPClientTransport | null = null;
private stdinBuffer: string = "";
private isReady: boolean = false;
private isShuttingDown: boolean = false;
constructor(options: MCPProxyOptions) {
this.endpoint = options.endpoint;
this.apiKey = options.apiKey;
this.profile = options.profile;
}
async start(): Promise<void> {
console.error(`🚀 Starting MCP proxy for package...`);
console.error(` Endpoint: ${this.endpoint}`);
if (this.profile) {
console.error(` Profile: ${this.profile}`);
}
await this.setupTransport();
this.setupStdinHandling();
this.setupProcessHandlers();
}
private async setupTransport(): Promise<void> {
try {
// Use the endpoint as provided (do not append /mcp)
const endpoint = this.endpoint;
// Create the transport URL with query parameters
const url = new URL(endpoint);
// Add API key as query parameter
url.searchParams.set("apiKey", this.apiKey);
// Add profile if provided
if (this.profile) {
url.searchParams.set("profile", this.profile);
}
console.error(`[DEBUG] Connecting to: ${url.toString()}`);
// Create the MCP transport
this.transport = new StreamableHTTPClientTransport(url);
// Set up message handler
this.transport.onmessage = (message: JSONRPCMessage) => {
try {
if ("error" in message) {
const errorMessage = message as JSONRPCError;
console.error(`[DEBUG] Received error: ${JSON.stringify(errorMessage)}`);
}
// Forward the message to stdout for Claude
console.log(JSON.stringify(message));
} catch (error) {
console.error(`[DEBUG] Error handling message:`, error);
}
};
// Set up error handler
this.transport.onerror = (error: Error) => {
console.error(`[DEBUG] Transport error: ${error.message}`);
};
// Set up close handler
this.transport.onclose = () => {
console.error(`[DEBUG] Transport closed`);
if (!this.isShuttingDown) {
process.exit(1);
}
};
// Start the transport
this.transport.start();
this.isReady = true;
console.error(`[DEBUG] Transport started successfully`);
} catch (error) {
console.error(`[DEBUG] Failed to setup transport:`, error);
process.exit(1);
}
}
private setupStdinHandling(): void {
// Handle stdin data
process.stdin.on("data", (data) => {
this.handleIncomingData(data.toString());
});
// Handle stdin end (client disconnect)
process.stdin.on("end", () => {
console.error(`[DEBUG] STDIN closed (client disconnected)`);
this.cleanup();
});
// Handle stdin errors
process.stdin.on("error", (error) => {
console.error(`[DEBUG] STDIN error:`, error);
this.cleanup();
});
}
private setupProcessHandlers(): void {
// Handle process termination signals
process.on("SIGINT", () => {
console.error(`[DEBUG] Received SIGINT, cleaning up...`);
this.cleanup();
});
process.on("SIGTERM", () => {
console.error(`[DEBUG] Received SIGTERM, cleaning up...`);
this.cleanup();
});
process.on("beforeExit", () => {
console.error(`[DEBUG] Process exiting, cleaning up...`);
this.cleanup();
});
}
private handleIncomingData(chunk: string): void {
if (!this.isReady || !this.transport) return;
this.stdinBuffer += chunk;
// Process complete JSON-RPC messages (line-delimited)
const lines = this.stdinBuffer.split(/\r?\n/);
this.stdinBuffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
this.processMessage(line.trim());
}
}
}
private async processMessage(messageStr: string): Promise<void> {
try {
const message = JSON.parse(messageStr) as JSONRPCMessage;
console.error(`[DEBUG] Forwarding message: ${JSON.stringify(message)}`);
if (this.transport) {
await this.transport.send(message);
}
} catch (error) {
console.error(`[DEBUG] Error processing message:`, error);
console.error(`[DEBUG] Invalid message: ${messageStr}`);
}
}
private cleanup(): void {
if (this.isShuttingDown) return;
this.isShuttingDown = true;
console.error(`[DEBUG] Cleaning up MCP proxy...`);
if (this.transport) {
try {
this.transport.close();
} catch (error) {
console.error(`[DEBUG] Error closing transport:`, error);
}
this.transport = null;
}
process.exit(0);
}
}