From b85b54fafe652ba15adfd708a8f295c5a7781ad8 Mon Sep 17 00:00:00 2001 From: andrew-paystack Date: Wed, 4 Mar 2026 11:39:24 +0300 Subject: [PATCH 1/3] setup npm distribution --- README.md | 18 ++- SETUP.md | 174 +++++++++++++++++++++++++++++ package.json | 23 +++- src/config.ts | 71 ++++++------ src/index.ts | 59 +++++++++- src/paystack-client.ts | 6 +- src/server.ts | 8 +- src/tools/index.ts | 5 +- src/tools/make-paystack-request.ts | 9 +- 9 files changed, 319 insertions(+), 54 deletions(-) create mode 100644 SETUP.md diff --git a/README.md b/README.md index d14d7b4..37dc681 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,13 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that e ## Quick Start -Clone the repo and build locally: +Install and run via npm (recommended): + +```bash +npx @paystack/mcp-server --api-key sk_test_your_key_here +``` + +Or for local development, clone and build: ```bash git clone https://github.com/PaystackOSS/paystack-mcp-server.git @@ -16,7 +22,7 @@ npm install npm run build ``` -Then configure your MCP client to use the built server (see [Client Integration](#client-integration)). +Then configure your MCP client to use the server (see [Client Integration](#client-integration)). ## Requirements @@ -28,7 +34,11 @@ Then configure your MCP client to use the built server (see [Client Integration] | Environment Variable | Purpose | | -------------------------- | ------------------------------------------------------ | -| `PAYSTACK_TEST_SECRET_KEY` | Your Paystack test secret key **(required)** | +| `PAYSTACK_TEST_SECRET_KEY` | Your Paystack test secret key (fallback if no CLI arg) | + +You can provide your API key in two ways: +1. **CLI argument (recommended):** `--api-key sk_test_...` +2. **Environment variable:** Set `PAYSTACK_TEST_SECRET_KEY` > **Security note:** Only test keys (`sk_test_*`) are allowed. The server validates this at startup and will reject live keys. @@ -36,7 +46,7 @@ Then configure your MCP client to use the built server (see [Client Integration] The Paystack MCP Server works with any MCP-compatible client. Below is the standard configuration schema used by most clients (Claude Desktop, ChatGPT Desktop, Cursor, Windsurf, etc.). -### Using a local build +### Using npm (recommended)\n\nFor npm-installed server:\n\n```json\n{\n \"mcpServers\": {\n \"paystack\": {\n \"command\": \"npx\",\n \"args\": [\"@paystack/mcp-server\", \"--api-key\", \"sk_test_...\"]\n }\n }\n}\n```\n\n### Using a local build If you've cloned and built the server locally: diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..b7aaaf7 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,174 @@ +# Editor Setup + +Configure the Paystack MCP Server in supported editors with secure `.env`-based API key management. + +- [Environment Setup](#environment-setup) +- [VS Code](#vs-code) +- [Cursor](#cursor) +- [Claude Desktop](#claude-desktop) + +--- + +## Environment Setup + +Create your environment file: + +1. **Copy the example file:** + + ```bash + cp .env.example .env + ``` + +2. **Add your Paystack test secret key:** + + ```env + PAYSTACK_TEST_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + ``` + +> [!IMPORTANT] +> - Only **test keys** (starting with `sk_test_`) are accepted. The server rejects live keys. +> - The `.env` file is already in `.gitignore`—never commit it to version control. + +--- + +## VS Code + +VS Code supports the `envFile` property, allowing you to load environment variables from a file instead of hardcoding them. + +### Configuration + +Create or update `.vscode/mcp.json` in your project: + +```json +{ + "servers": { + "paystack": { + "command": "node", + "args": ["/path/to/paystack-mcp/build/index.js"], + "envFile": "${workspaceFolder}/.env" + } + } +} +``` + +> [!NOTE] +> Replace `/path/to/paystack-mcp` with the actual path to your cloned repository. + +### Reload the MCP Server + +After saving the configuration, reload VS Code or run the **"MCP: Restart Server"** command from the Command Palette. + +--- + +## Cursor + +Cursor supports both `envFile` and environment variable interpolation via `${env:VAR_NAME}`. + +### Configuration Locations + +| Scope | File Path | +| ------- | ------------------------ | +| Project | `.cursor/mcp.json` | +| Global | `~/.cursor/mcp.json` | + +### Using `envFile` (Recommended) + +Create `.cursor/mcp.json` in your project: + +```json +{ + "mcpServers": { + "paystack": { + "command": "node", + "args": ["/path/to/paystack-mcp/build/index.js"], + "envFile": "${workspaceFolder}/.env" + } + } +} +``` + +--- + +## Claude Desktop + +Claude Desktop uses an inline `env` object for environment variables. It does not support `envFile`. + +### Configuration Location + +| OS | File Path | +| ------- | ------------------------------------------------------------ | +| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | +| Windows | `%APPDATA%\Claude\claude_desktop_config.json` | + +### Approach A: Inline Environment Variables (Simple) + +Edit your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "paystack": { + "command": "node", + "args": ["/path/to/paystack-mcp/build/index.js"], + "env": { + "PAYSTACK_SECRET_KEY": "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } + } +} +``` + +> [!WARNING] +> This approach stores your API key directly in the config file. Ensure this file is not shared or committed to version control. + +### Approach B: Using a Wrapper Script (Secure) + +For better security, create a shell script that loads your `.env` file before starting the server. + +1. **Create a wrapper script** (e.g., `run-paystack-mcp.sh`): + + ```bash + #!/bin/bash + set -a + source /path/to/paystack-mcp/.env + set +a + exec node /path/to/paystack-mcp/build/index.js + ``` + +2. **Make it executable:** + + ```bash + chmod +x /path/to/run-paystack-mcp.sh + ``` + +3. **Update your Claude Desktop config:** + + ```json + { + "mcpServers": { + "paystack": { + "command": "/path/to/run-paystack-mcp.sh" + } + } + } + ``` + +--- + +## Troubleshooting + +### Server not starting + +- Verify Node.js v18+ is installed: `node --version` +- Check the path to `build/index.js` is correct +- Ensure your `.env` file exists and contains a valid `sk_test_*` key + +### Environment variables not loading + +- For VS Code/Cursor: Confirm `envFile` path is correct and the file exists +- For Claude Desktop: Restart the application after config changes + +### "Invalid API key" errors + +- Ensure your key starts with `sk_test_` (live will be rejected) +- Check for trailing whitespace in your `.env` file diff --git a/package.json b/package.json index 693b89f..54846bd 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,14 @@ { - "name": "paystack-mcp", + "name": "@paystack/mcp-server", "version": "0.0.1", - "description": "", + "description": "Model Context Protocol (MCP) server for Paystack API integration", + "mcpName": "io.github.PaystackOSS/paystack", + "repository": { + "type": "git", + "url": "https://github.com/PaystackOSS/paystack-mcp-server.git" + }, "bin": { - "paystack": "./build/index.js" + "paystack-mcp": "./build/index.js" }, "scripts": { "build": "tsc && cp -r src/data build/", @@ -15,14 +20,20 @@ "files": [ "build" ], - "keywords": [], + "keywords": [ + "paystack", + "mcp", + "model-context-protocol", + "api", + "payment", + "integration" + ], "author": "Andrew-Paystack", "license": "MIT", "dependencies": { "@modelcontextprotocol/inspector": "^0.18.0", "@modelcontextprotocol/sdk": "^1.26.0", - "dotenv": "^17.2.3", - "zod": "^4.3.6" + "dotenv": "^17.2.3" }, "devDependencies": { "@apidevtools/swagger-parser": "^12.1.0", diff --git a/src/config.ts b/src/config.ts index e9a3e33..1778454 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,41 +1,46 @@ import dotenv from 'dotenv'; -import { z } from 'zod'; // Load environment variables from .env file -dotenv.config(); +dotenv.config({quiet: true}); -// Define schema for required environment variables -const envSchema = z.object({ - PAYSTACK_TEST_SECRET_KEY: z.string().min(30, 'PAYSTACK_TEST_SECRET_KEY is required').refine(val => val.startsWith('sk_test_'), { - message: 'PAYSTACK_TEST_SECRET_KEY must begin with "sk_test_. No live keys allowed."', - }), - NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), - LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), -}); - -// Validate environment variables -function validateEnv() { - try { - return envSchema.parse({ - PAYSTACK_TEST_SECRET_KEY: process.env.PAYSTACK_TEST_SECRET_KEY, - NODE_ENV: process.env.NODE_ENV || 'development', - LOG_LEVEL: process.env.LOG_LEVEL || 'info', - }); - } catch (error) { - if (error instanceof z.ZodError) { - // Environment validation failed - exit silently - process.exit(1); - } - throw error; +// Get configuration with optional CLI API key +export function getConfig(cliApiKey?: string) { + const apiKey = cliApiKey || process.env.PAYSTACK_TEST_SECRET_KEY; + + if (!apiKey) { + console.error('Error: PAYSTACK_TEST_SECRET_KEY is required'); + process.exit(1); + } + + if (!apiKey.startsWith('sk_test_')) { + console.error('Error: PAYSTACK_TEST_SECRET_KEY must begin with "sk_test_". No live keys allowed.'); + process.exit(1); + } + + if (apiKey.length < 30) { + console.error('Error: PAYSTACK_TEST_SECRET_KEY appears to be too short'); + process.exit(1); } + + return { + PAYSTACK_TEST_SECRET_KEY: apiKey, + NODE_ENV: (process.env.NODE_ENV as 'development' | 'production' | 'test') || 'development', + LOG_LEVEL: (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') || 'info', + }; } -// Export validated configuration -export const config = validateEnv(); +// Export validated configuration (for backward compatibility, will use env var) +export const config = getConfig(); + +// Paystack API configuration factory +export function createPaystackConfig(cliApiKey?: string) { + const cfg = getConfig(cliApiKey); + return { + baseURL: 'https://api.paystack.co', + secretKey: cfg.PAYSTACK_TEST_SECRET_KEY, + timeout: 30000, // 30 seconds + } as const; +} -// Paystack API configuration -export const paystackConfig = { - baseURL: 'https://api.paystack.co', - secretKey: config.PAYSTACK_TEST_SECRET_KEY, - timeout: 30000, // 30 seconds -} as const; +// Default paystack config (for backward compatibility) +export const paystackConfig = createPaystackConfig(); diff --git a/src/index.ts b/src/index.ts index 168c49b..614e0ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,64 @@ +#!/usr/bin/env node import { startServer } from "./server"; +// Simple CLI argument parsing +function parseApiKey(): string | undefined { + const args = process.argv; + const apiKeyIndex = args.findIndex(arg => arg === '--api-key'); + + if (apiKeyIndex !== -1 && apiKeyIndex + 1 < args.length) { + return args[apiKeyIndex + 1]; + } + + return undefined; +} + +// Show help message +function showHelp() { + console.log(` +Paystack MCP Server + +Usage: + npx @paystack/mcp-server --api-key + +Options: + --api-key Your Paystack test secret key (starts with sk_test_) + --help, -h Show this help message + +Environment Variables: + PAYSTACK_TEST_SECRET_KEY Fallback if --api-key not provided + +Examples: + npx @paystack/mcp-server --api-key sk_test_1234567890abcdef + PAYSTACK_TEST_SECRET_KEY=sk_test_... npx @paystack/mcp-server +`); +} + async function main() { - await startServer(); + // Handle help flag + if (process.argv.includes('--help') || process.argv.includes('-h')) { + showHelp(); + process.exit(0); + } + + // Parse API key from CLI + const cliApiKey = parseApiKey(); + + // Validate API key format if provided via CLI + if (cliApiKey && !cliApiKey.startsWith('sk_test_')) { + console.error('Error: API key must start with "sk_test_". Only test keys are allowed.'); + process.exit(1); + } + + // Check if we have an API key from CLI or environment + if (!cliApiKey && !process.env.PAYSTACK_TEST_SECRET_KEY) { + console.error('Error: Paystack API key required.'); + console.error('Provide via --api-key argument or PAYSTACK_TEST_SECRET_KEY environment variable.'); + showHelp(); + process.exit(1); + } + + await startServer(cliApiKey); } main().catch((error) => { diff --git a/src/paystack-client.ts b/src/paystack-client.ts index ec81928..30c8074 100644 --- a/src/paystack-client.ts +++ b/src/paystack-client.ts @@ -4,7 +4,7 @@ import { paystackConfig } from "./config"; const PAYSTACK_BASE_URL = paystackConfig.baseURL; const USER_AGENT = process.env.USER_AGENT || 'Paystack-MCP-Server/0.0.1'; -class PaystackClient { +export class PaystackClient { private baseUrl: string; private secretKey: string; private userAgent: string; @@ -89,8 +89,10 @@ class PaystackClient { throw error; } - } } +} + +// Export singleton instance for backward compatibility export const paystackClient = new PaystackClient( paystackConfig.secretKey ); diff --git a/src/server.ts b/src/server.ts index 088e504..65abadf 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,7 +5,7 @@ import { OpenAPIParser } from "./openapi-parser"; import { registerAllTools } from "./tools"; import { registerAllResources } from "./resources"; -async function createServer() { +async function createServer(cliApiKey?: string) { const server = new McpServer({ name: "paystack", version: "0.0.1", @@ -16,14 +16,14 @@ async function createServer() { await openapi.parse(); - registerAllTools(server, openapi); + registerAllTools(server, openapi, cliApiKey); registerAllResources(server, openapi); return server; } -export async function startServer() { - const server = await createServer(); +export async function startServer(cliApiKey?: string) { + const server = await createServer(cliApiKey); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Paystack MCP Server running on stdio..."); diff --git a/src/tools/index.ts b/src/tools/index.ts index 194c20a..e63eb49 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -5,8 +5,9 @@ import { registerMakePaystackRequestTool } from "./make-paystack-request"; export function registerAllTools( server: McpServer, - openapi: OpenAPIParser + openapi: OpenAPIParser, + cliApiKey?: string ) { registerGetPaystackOperationTool(server, openapi); - registerMakePaystackRequestTool(server); + registerMakePaystackRequestTool(server, cliApiKey); } diff --git a/src/tools/make-paystack-request.ts b/src/tools/make-paystack-request.ts index 74d918e..6adce0d 100644 --- a/src/tools/make-paystack-request.ts +++ b/src/tools/make-paystack-request.ts @@ -1,8 +1,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import * as z from "zod"; -import { paystackClient } from "../paystack-client"; +import { PaystackClient } from "../paystack-client"; +import { createPaystackConfig } from "../config"; -export function registerMakePaystackRequestTool(server: McpServer) { +export function registerMakePaystackRequestTool(server: McpServer, cliApiKey?: string) { + // Create PaystackClient with CLI API key or fallback to environment + const config = createPaystackConfig(cliApiKey); + const paystackClient = new PaystackClient(config.secretKey); + server.registerTool( "make_paystack_request", { From c3ca7ae40c703278e861d2f16dbeba10fda65079 Mon Sep 17 00:00:00 2001 From: andrew-paystack Date: Wed, 4 Mar 2026 18:08:22 +0300 Subject: [PATCH 2/3] clean up and add tests for missing API key --- README.md | 17 ++++++++- package-lock.json | 16 ++------ package.json | 5 ++- src/config.ts | 6 --- src/index.ts | 11 ++---- src/logger.ts | 15 ++++++-- src/paystack-client.ts | 15 +++++--- test/make-paystack-request-tool.spec.ts | 51 ++++++++++++++++++++++++- test/paystack-client.spec.ts | 5 ++- 9 files changed, 101 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 37dc681..ba6ac7a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,22 @@ You can provide your API key in two ways: The Paystack MCP Server works with any MCP-compatible client. Below is the standard configuration schema used by most clients (Claude Desktop, ChatGPT Desktop, Cursor, Windsurf, etc.). -### Using npm (recommended)\n\nFor npm-installed server:\n\n```json\n{\n \"mcpServers\": {\n \"paystack\": {\n \"command\": \"npx\",\n \"args\": [\"@paystack/mcp-server\", \"--api-key\", \"sk_test_...\"]\n }\n }\n}\n```\n\n### Using a local build +### Using npm (recommended) + +For npm-installed server: + +```json +{ + "mcpServers": { + "paystack": { + "command": "npx", + "args": ["@paystack/mcp-server", "--api-key", "sk_test_..."] + } + } +} +``` + +### Using a local build If you've cloned and built the server locally: diff --git a/package-lock.json b/package-lock.json index b1768f8..4538ad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "paystack-mcp", + "name": "@paystack/mcp-server", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "paystack-mcp", + "name": "@paystack/mcp-server", "version": "0.0.1", "license": "MIT", "dependencies": { @@ -15,7 +15,7 @@ "zod": "^4.3.6" }, "bin": { - "paystack": "build/index.js" + "paystack-mcp-server": "build/index.js" }, "devDependencies": { "@apidevtools/swagger-parser": "^12.1.0", @@ -1821,7 +1821,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2665,7 +2664,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3041,7 +3039,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -3567,8 +3564,7 @@ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/p-limit": { "version": "3.1.0", @@ -3771,7 +3767,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3784,7 +3779,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4533,7 +4527,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4912,7 +4905,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 54846bd..450cc43 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "https://github.com/PaystackOSS/paystack-mcp-server.git" }, "bin": { - "paystack-mcp": "./build/index.js" + "paystack-mcp-server": "./build/index.js" }, "scripts": { "build": "tsc && cp -r src/data build/", @@ -33,7 +33,8 @@ "dependencies": { "@modelcontextprotocol/inspector": "^0.18.0", "@modelcontextprotocol/sdk": "^1.26.0", - "dotenv": "^17.2.3" + "dotenv": "^17.2.3", + "zod": "^4.3.6" }, "devDependencies": { "@apidevtools/swagger-parser": "^12.1.0", diff --git a/src/config.ts b/src/config.ts index 1778454..189c98e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,9 +29,6 @@ export function getConfig(cliApiKey?: string) { }; } -// Export validated configuration (for backward compatibility, will use env var) -export const config = getConfig(); - // Paystack API configuration factory export function createPaystackConfig(cliApiKey?: string) { const cfg = getConfig(cliApiKey); @@ -41,6 +38,3 @@ export function createPaystackConfig(cliApiKey?: string) { timeout: 30000, // 30 seconds } as const; } - -// Default paystack config (for backward compatibility) -export const paystackConfig = createPaystackConfig(); diff --git a/src/index.ts b/src/index.ts index 614e0ac..ae61da1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node -import { startServer } from "./server"; // Simple CLI argument parsing function parseApiKey(): string | undefined { @@ -41,15 +39,12 @@ async function main() { process.exit(0); } + const { startServer } = await import("./server"); + // Parse API key from CLI const cliApiKey = parseApiKey(); - // Validate API key format if provided via CLI - if (cliApiKey && !cliApiKey.startsWith('sk_test_')) { - console.error('Error: API key must start with "sk_test_". Only test keys are allowed.'); - process.exit(1); - } - + // Check if we have an API key from CLI or environment if (!cliApiKey && !process.env.PAYSTACK_TEST_SECRET_KEY) { console.error('Error: Paystack API key required.'); diff --git a/src/logger.ts b/src/logger.ts index 068035b..a6aa82d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,4 @@ -import { config } from './config.js'; +import { getConfig } from './config.js'; // Define log levels export enum LogLevel { DEBUG = 'debug', @@ -92,8 +92,8 @@ function redactSensitiveData(obj: any): any { class Logger { private currentLogLevel: LogLevel; - constructor() { - this.currentLogLevel = config.LOG_LEVEL as LogLevel; + constructor(logLevel?: LogLevel) { + this.currentLogLevel = logLevel || LogLevel.INFO; } private shouldLog(level: LogLevel): boolean { @@ -175,4 +175,13 @@ class Logger { } } +/** + * Create a logger instance with configuration + */ +export function createLogger(cliApiKey?: string): Logger { + const config = getConfig(cliApiKey); + return new Logger(config.LOG_LEVEL as LogLevel); +} + +// Default logger instance for backward compatibility (uses environment variable) export const logger = new Logger(); diff --git a/src/paystack-client.ts b/src/paystack-client.ts index 30c8074..b539751 100644 --- a/src/paystack-client.ts +++ b/src/paystack-client.ts @@ -1,7 +1,7 @@ import { PaystackResponse, PaystackError } from "./types"; -import { paystackConfig } from "./config"; +import { createPaystackConfig } from "./config"; -const PAYSTACK_BASE_URL = paystackConfig.baseURL; +const PAYSTACK_BASE_URL = 'https://api.paystack.co'; const USER_AGENT = process.env.USER_AGENT || 'Paystack-MCP-Server/0.0.1'; export class PaystackClient { @@ -92,7 +92,10 @@ export class PaystackClient { } } -// Export singleton instance for backward compatibility -export const paystackClient = new PaystackClient( - paystackConfig.secretKey -); +/** + * Create a PaystackClient instance with configuration + */ +export function createPaystackClient(cliApiKey?: string): PaystackClient { + const config = createPaystackConfig(cliApiKey); + return new PaystackClient(config.secretKey, config.baseURL, undefined, config.timeout); +} diff --git a/test/make-paystack-request-tool.spec.ts b/test/make-paystack-request-tool.spec.ts index 82c64c8..536fdb7 100644 --- a/test/make-paystack-request-tool.spec.ts +++ b/test/make-paystack-request-tool.spec.ts @@ -17,7 +17,8 @@ describe("MakePaystackRequestTool", () => { } } as any; - registerMakePaystackRequestTool(server); + // Pass a test API key to avoid environment variable requirement + registerMakePaystackRequestTool(server, "sk_test_1234567890abcdef1234567890abcdef12345678"); }); it("should return isError: true for non-JSON responses", async () => { @@ -145,4 +146,52 @@ describe("MakePaystackRequestTool", () => { } }); }); + describe("Missing API Key Validation", () => { + let server: McpServer; + let originalExit: typeof process.exit; + let exitCode: number | undefined; + let consoleErrors: string[] = []; + let originalConsoleError: typeof console.error; + + beforeEach(() => { + // Mock process.exit to capture exit calls + exitCode = undefined; + originalExit = process.exit; + process.exit = ((code?: number) => { + exitCode = code || 0; + throw new Error(`Process would exit with code ${exitCode}`); + }) as any; + + // Mock console.error to capture error messages + consoleErrors = []; + originalConsoleError = console.error; + console.error = (...args: any[]) => { + consoleErrors.push(args.join(' ')); + }; + + server = { + registerTool: (name: string, config: any, handler: any) => { } + } as any; + }); + + afterEach(() => { + process.exit = originalExit; + console.error = originalConsoleError; + delete process.env.PAYSTACK_TEST_SECRET_KEY; + }); + + it("should fail when no API key provided via CLI or environment", () => { + // Ensure no environment variable is set + delete process.env.PAYSTACK_TEST_SECRET_KEY; + + try { + registerMakePaystackRequestTool(server); // No cliApiKey parameter + assert.fail("Expected registerMakePaystackRequestTool to throw an error"); + } catch (error: any) { + assert.ok(error.message.includes("Process would exit with code 1")); + assert.strictEqual(exitCode, 1); + assert.ok(consoleErrors.some(msg => msg.includes("PAYSTACK_TEST_SECRET_KEY is required"))); + } + }); + }); }); diff --git a/test/paystack-client.spec.ts b/test/paystack-client.spec.ts index 39f82be..ce98c8d 100644 --- a/test/paystack-client.spec.ts +++ b/test/paystack-client.spec.ts @@ -1,7 +1,10 @@ import assert from "node:assert"; -import { paystackClient } from "../src/paystack-client.js"; +import { createPaystackClient } from "../src/paystack-client.js"; describe("PaystackClient", () => { + // Use a test API key for the test client + const paystackClient = createPaystackClient("sk_test_1234567890abcdef1234567890abcdef12345678"); + describe("makeRequest - Non-JSON Response Handling", () => { it("should throw a descriptive error for HTML error responses", async () => { // This test validates that non-JSON responses (like HTML error pages) From cb2c79a6a7c04cf963d955db9a21e1cd094bcba6 Mon Sep 17 00:00:00 2001 From: andrew-paystack Date: Wed, 4 Mar 2026 18:15:10 +0300 Subject: [PATCH 3/3] chore: remove setup & update readme --- README.md | 1 - SETUP.md | 174 ------------------------------------------------------ 2 files changed, 175 deletions(-) delete mode 100644 SETUP.md diff --git a/README.md b/README.md index ba6ac7a..ef27483 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,6 @@ The Paystack MCP Server exposes the **entire Paystack API** to AI assistants by | Tool | Description | | ------------------------ | ------------------------------------------------------------------ | | `get_paystack_operation` | Fetch operation details (method, path, parameters) by operation ID | -| `get_paystack_operation_guided` | Infers the operation ID from prompt | | `make_paystack_request` | Execute a Paystack API request | ### Available Resources diff --git a/SETUP.md b/SETUP.md deleted file mode 100644 index b7aaaf7..0000000 --- a/SETUP.md +++ /dev/null @@ -1,174 +0,0 @@ -# Editor Setup - -Configure the Paystack MCP Server in supported editors with secure `.env`-based API key management. - -- [Environment Setup](#environment-setup) -- [VS Code](#vs-code) -- [Cursor](#cursor) -- [Claude Desktop](#claude-desktop) - ---- - -## Environment Setup - -Create your environment file: - -1. **Copy the example file:** - - ```bash - cp .env.example .env - ``` - -2. **Add your Paystack test secret key:** - - ```env - PAYSTACK_TEST_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx - ``` - -> [!IMPORTANT] -> - Only **test keys** (starting with `sk_test_`) are accepted. The server rejects live keys. -> - The `.env` file is already in `.gitignore`—never commit it to version control. - ---- - -## VS Code - -VS Code supports the `envFile` property, allowing you to load environment variables from a file instead of hardcoding them. - -### Configuration - -Create or update `.vscode/mcp.json` in your project: - -```json -{ - "servers": { - "paystack": { - "command": "node", - "args": ["/path/to/paystack-mcp/build/index.js"], - "envFile": "${workspaceFolder}/.env" - } - } -} -``` - -> [!NOTE] -> Replace `/path/to/paystack-mcp` with the actual path to your cloned repository. - -### Reload the MCP Server - -After saving the configuration, reload VS Code or run the **"MCP: Restart Server"** command from the Command Palette. - ---- - -## Cursor - -Cursor supports both `envFile` and environment variable interpolation via `${env:VAR_NAME}`. - -### Configuration Locations - -| Scope | File Path | -| ------- | ------------------------ | -| Project | `.cursor/mcp.json` | -| Global | `~/.cursor/mcp.json` | - -### Using `envFile` (Recommended) - -Create `.cursor/mcp.json` in your project: - -```json -{ - "mcpServers": { - "paystack": { - "command": "node", - "args": ["/path/to/paystack-mcp/build/index.js"], - "envFile": "${workspaceFolder}/.env" - } - } -} -``` - ---- - -## Claude Desktop - -Claude Desktop uses an inline `env` object for environment variables. It does not support `envFile`. - -### Configuration Location - -| OS | File Path | -| ------- | ------------------------------------------------------------ | -| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | -| Windows | `%APPDATA%\Claude\claude_desktop_config.json` | - -### Approach A: Inline Environment Variables (Simple) - -Edit your `claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "paystack": { - "command": "node", - "args": ["/path/to/paystack-mcp/build/index.js"], - "env": { - "PAYSTACK_SECRET_KEY": "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - } - } - } -} -``` - -> [!WARNING] -> This approach stores your API key directly in the config file. Ensure this file is not shared or committed to version control. - -### Approach B: Using a Wrapper Script (Secure) - -For better security, create a shell script that loads your `.env` file before starting the server. - -1. **Create a wrapper script** (e.g., `run-paystack-mcp.sh`): - - ```bash - #!/bin/bash - set -a - source /path/to/paystack-mcp/.env - set +a - exec node /path/to/paystack-mcp/build/index.js - ``` - -2. **Make it executable:** - - ```bash - chmod +x /path/to/run-paystack-mcp.sh - ``` - -3. **Update your Claude Desktop config:** - - ```json - { - "mcpServers": { - "paystack": { - "command": "/path/to/run-paystack-mcp.sh" - } - } - } - ``` - ---- - -## Troubleshooting - -### Server not starting - -- Verify Node.js v18+ is installed: `node --version` -- Check the path to `build/index.js` is correct -- Ensure your `.env` file exists and contains a valid `sk_test_*` key - -### Environment variables not loading - -- For VS Code/Cursor: Confirm `envFile` path is correct and the file exists -- For Claude Desktop: Restart the application after config changes - -### "Invalid API key" errors - -- Ensure your key starts with `sk_test_` (live will be rejected) -- Check for trailing whitespace in your `.env` file