Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -28,14 +34,33 @@ 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.

## 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 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:
Expand Down Expand Up @@ -91,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
Expand Down
16 changes: 4 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -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-server": "./build/index.js"
},
"scripts": {
"build": "tsc && cp -r src/data build/",
Expand All @@ -15,7 +20,14 @@
"files": [
"build"
],
"keywords": [],
"keywords": [
"paystack",
"mcp",
"model-context-protocol",
"api",
"payment",
"integration"
],
"author": "Andrew-Paystack",
"license": "MIT",
"dependencies": {
Expand Down
67 changes: 33 additions & 34 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
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();

// Paystack API configuration
export const paystackConfig = {
baseURL: 'https://api.paystack.co',
secretKey: config.PAYSTACK_TEST_SECRET_KEY,
timeout: 30000, // 30 seconds
} as const;
// 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;
}
56 changes: 54 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,59 @@
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 <your-test-secret-key>

Options:
--api-key <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);
}

const { startServer } = await import("./server");

// Parse API key from CLI
const cliApiKey = parseApiKey();


// Check if we have an API key from CLI or environment
if (!cliApiKey && !process.env.PAYSTACK_TEST_SECRET_KEY) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to what is being done in the config file. Can we consolidate?

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) => {
Expand Down
15 changes: 12 additions & 3 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { config } from './config.js';
import { getConfig } from './config.js';
// Define log levels
export enum LogLevel {
DEBUG = 'debug',
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
19 changes: 12 additions & 7 deletions src/paystack-client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
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';

class PaystackClient {
export class PaystackClient {
private baseUrl: string;
private secretKey: string;
private userAgent: string;
Expand Down Expand Up @@ -89,8 +89,13 @@ class PaystackClient {
throw error;
}

}
}
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);
}
Loading