@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
JavaScript
/**
* 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