UNPKG

@compligent-mcp/csf

Version:

Compligent MCP Client - NIST CSF 2.0 with OAuth 2.1 PKCE authentication via Unified Gateway

242 lines 10.1 kB
#!/usr/bin/env node /** * Compligent MCP Client - NIST CSF 2.0 * * This client connects to the hosted CSF 2.0 service using StreamableHTTP transport. * It uses StreamableHTTPClientTransport to connect to the gateway, which proxies to the backend server. * * Architecture: * csf-cli (stdio) → local MCP Server → StreamableHTTP Client → Gateway → CSF Server (StreamableHTTP) */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { readFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import { homedir } from "os"; import { OAuthClient } from "./oauth-client.js"; // Get package version dynamically const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")); const CLIENT_VERSION = packageJson.version; // Configuration - Gateway proxies to CSF server's StreamableHTTP transport const GATEWAY_BASE_URL = "https://compligent-oauth-gateway-prod.compli.workers.dev"; const MCP_ENDPOINT = `${GATEWAY_BASE_URL}/mcp/csf`; // Gateway proxy endpoint // OAuth Configuration const OAUTH_CLIENT_ID = "uVR9_YOBzN63KGqZ"; // Shared dynamically registered client const OAUTH_REDIRECT_URI = "http://localhost:3333/callback"; const OAUTH_SCOPES = ["mcp:read", "mcp:execute", "user:profile"]; class CSFClient { localServer; remoteClient = null; oauthClient; authPromise = null; constructor() { this.localServer = new Server({ name: "@compligent-mcp/csf", version: CLIENT_VERSION, }, { capabilities: { tools: {}, prompts: {}, }, }); // Initialize OAuth client this.oauthClient = new OAuthClient({ gatewayUrl: GATEWAY_BASE_URL, clientId: OAUTH_CLIENT_ID, redirectUri: OAUTH_REDIRECT_URI, scopes: OAUTH_SCOPES }); this.setupHandlers(); } async loadToken() { // Try environment variables first (for backward compatibility) let token = process.env.COMPLIGENT_TOKEN || process.env.COMPLIGENT_API_KEY; if (token) { console.error(' ✓ Using token from environment variable'); return token; } // Try loading from legacy ~/.compligent/token file try { const tokenPath = join(homedir(), '.compligent', 'token'); token = readFileSync(tokenPath, 'utf8').trim(); if (token) { console.error(' ✓ Loaded token from ~/.compligent/token (legacy)'); return token; } } catch (error) { // Token file doesn't exist or can't be read } // Use OAuth flow to get token console.error(' 🔐 No legacy token found, using OAuth authentication...'); try { const accessToken = await this.oauthClient.getAccessToken(); return accessToken; } catch (error) { console.error(` ❌ OAuth authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); return null; } } async ensureRemoteClient() { if (this.remoteClient) { return this.remoteClient; } // Get authentication token from env or OAuth const authToken = await this.loadToken(); // Create StreamableHTTP transport for connection // The gateway at /mcp/csf proxies to the CSF server's /mcp endpoint // StreamableHTTPClientTransport uses POST with JSON-RPC over HTTP const headers = { "User-Agent": `@compligent-mcp/csf/${CLIENT_VERSION}`, "Content-Type": "application/json", "Accept": "application/json, text/event-stream" }; if (authToken) { headers["Authorization"] = `Bearer ${authToken}`; } const transport = new StreamableHTTPClientTransport(new URL(MCP_ENDPOINT), { requestInit: { headers } }); // Create and connect remote client this.remoteClient = new Client({ name: "@compligent-mcp/csf-proxy", version: CLIENT_VERSION, }, { capabilities: {} }); await this.remoteClient.connect(transport); console.error(' ✓ Connected to remote CSF server via StreamableHTTP'); return this.remoteClient; } setupHandlers() { // List tools - proxy to remote server this.localServer.setRequestHandler(ListToolsRequestSchema, async () => { try { const client = await this.ensureRemoteClient(); const response = await client.listTools(); console.error(` 📋 Received ${response.tools.length} tools from remote server`); return response; } catch (error) { console.error("Failed to list tools:", error); return { tools: [] }; } }); // Call tool - proxy to remote server this.localServer.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { const client = await this.ensureRemoteClient(); const result = await client.callTool({ name, arguments: args || {} }); return result; } catch (error) { console.error(`Failed to call tool ${name}:`, error); // Return a user-friendly error message return { content: [ { type: "text", text: `Error: Unable to connect to Compligent API Gateway. Please check your internet connection and try again.\\n\\nTechnical details: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } }); // List prompts - proxy to remote server this.localServer.setRequestHandler(ListPromptsRequestSchema, async () => { try { const client = await this.ensureRemoteClient(); const response = await client.listPrompts(); return response; } catch (error) { console.error("Failed to list prompts:", error); return { prompts: [] }; } }); // Get prompt - proxy to remote server this.localServer.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { const client = await this.ensureRemoteClient(); const result = await client.getPrompt({ name, arguments: args || {} }); return result; } catch (error) { console.error(`Failed to get prompt ${name}:`, error); // Return a user-friendly error message return { description: `Error: Unable to connect to Compligent API Gateway. Please check your internet connection and try again.`, messages: [ { role: "assistant", content: { type: "text", text: `Technical details: ${error instanceof Error ? error.message : 'Unknown error'}`, }, }, ], }; } }); } async start() { const transport = new StdioServerTransport(); await this.localServer.connect(transport); // Log startup info to stderr (won't interfere with MCP protocol on stdout) console.error(`🚀 Compligent NIST CSF 2.0 MCP Client v${CLIENT_VERSION}`); console.error(` Using HTTP+SSE transport to: ${GATEWAY_BASE_URL}`); console.error(` Ready to proxy 30 tools + 13 prompts via MCP remote connection`); // Start authentication in background - don't block this.authPromise = this.loadToken(); // Don't await - let it complete in background this.authPromise.then(token => { if (token) { console.error(' ✅ Authenticated with OAuth Gateway (full access)'); } else { console.error(' ❌ Authentication failed - MCP server will not function'); console.error(' ℹ Please try running the client again to re-authenticate'); } }); } async cleanup() { if (this.remoteClient) { await this.remoteClient.close(); this.remoteClient = null; } } } // Error handling const client = new CSFClient(); process.on("SIGINT", async () => { console.error("\\n👋 Shutting down Compligent CSF 2.0 client..."); await client.cleanup(); process.exit(0); }); process.on("uncaughtException", async (error) => { console.error("💥 Uncaught Exception:", error); await client.cleanup(); process.exit(1); }); process.on("unhandledRejection", async (reason) => { console.error("💥 Unhandled Rejection:", reason); await client.cleanup(); process.exit(1); }); // Start the client client.start().catch((error) => { console.error("Failed to start CSF 2.0 client:", error); process.exit(1); }); //# sourceMappingURL=index.js.map