Skip to content

Commit 3e4a906

Browse files
link tool to HTTP client
2 parents 831dd84 + 26d6029 commit 3e4a906

12 files changed

Lines changed: 488 additions & 201 deletions

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
PAYSTACK_BASE_URL="https://api.paystack.co"
2-
USER_AGENT="paystack-mcp/1.0"
2+
USER_AGENT="paystack-mcp/1.0"
3+
PAYSTACK_TEST_SECRET_KEY="sk_secret_key_here"

package-lock.json

Lines changed: 87 additions & 191 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@
2121
"dependencies": {
2222
"@modelcontextprotocol/inspector": "^0.18.0",
2323
"@modelcontextprotocol/sdk": "^1.25.2",
24+
"dotenv": "^17.2.3",
2425
"zod": "^4.3.6"
2526
},
2627
"devDependencies": {
2728
"@apidevtools/swagger-parser": "^12.1.0",
2829
"@types/mocha": "^10.0.10",
2930
"@types/node": "^25.0.7",
3031
"js-yaml": "^4.1.1",
31-
"mocha": "^11.7.5",
32+
"mocha": "^11.3.0",
3233
"openapi-types": "^12.1.3",
3334
"tsx": "^4.21.0",
3435
"typescript": "^5.9.3"

readme.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Paystack MCP Server
2+
3+
This project implements a server for Paystack's [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server.
4+
5+
6+
## Requirements
7+
8+
- Node.js (v14+ recommended)
9+
- npm or yarn
10+
11+
## Setup
12+
13+
1. Clone the repository:
14+
```
15+
git clone https://github.com/yourusername/paystack-mcp-server.git
16+
cd paystack-mcp
17+
```
18+
19+
2. Install dependencies:
20+
```
21+
npm install
22+
```
23+
24+
3. Configure environment variables:
25+
- Copy `.env.example` to `.env` and update with your Paystack credentials and server settings.
26+
27+
4. Start the server:
28+
```
29+
npm start
30+
```
31+
32+
## Contributing
33+
34+
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
35+
36+
## License
37+
38+
MIT

src/config.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import dotenv from 'dotenv';
2+
import { z } from 'zod';
3+
4+
// Load environment variables from .env file
5+
dotenv.config();
6+
7+
// Define schema for required environment variables
8+
const envSchema = z.object({
9+
PAYSTACK_SECRET_KEY_TEST: z.string().min(30, 'PAYSTACK_SECRET_KEY_TEST is required').refine(val => val.startsWith('sk_test_'), {
10+
message: 'PAYSTACK_SECRET_KEY_TEST must begin with "sk_test_. No live keys allowed."',
11+
}),
12+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
13+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
14+
});
15+
16+
// Validate environment variables
17+
function validateEnv() {
18+
try {
19+
return envSchema.parse({
20+
PAYSTACK_SECRET_KEY_TEST: process.env.PAYSTACK_SECRET_KEY_TEST,
21+
NODE_ENV: process.env.NODE_ENV || 'development',
22+
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
23+
});
24+
} catch (error) {
25+
if (error instanceof z.ZodError) {
26+
// Environment validation failed - exit silently
27+
process.exit(1);
28+
}
29+
throw error;
30+
}
31+
}
32+
33+
// Export validated configuration
34+
export const config = validateEnv();
35+
36+
// Paystack API configuration
37+
export const paystackConfig = {
38+
baseURL: 'https://api.paystack.co',
39+
secretKey: config.PAYSTACK_SECRET_KEY_TEST,
40+
timeout: 30000, // 30 seconds
41+
} as const;

src/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import path from "path";
44
import * as z from "zod";
55
import { OpenAPIParser } from "./openapi-parser";
66
import { CreateMessageResultSchema } from "@modelcontextprotocol/sdk/types.js";
7+
import { paystackClient } from "./paystack-client";
78

8-
const PAYSTACK_BASE_URL = process.env.PAYSTACK_BASE_URL || "https://api.paystack.co";
9-
const USER_AGENT = process.env.USER_AGENT || "paystack-mcp/1.0";
9+
// const PAYSTACK_BASE_URL = process.env.PAYSTACK_BASE_URL || "https://api.paystack.co";
10+
// const USER_AGENT = process.env.USER_AGENT || "paystack-mcp/1.0";
1011

1112
// Create server instance
1213
const server = new McpServer({
@@ -95,12 +96,19 @@ async function initializeServer() {
9596
console.log("Request received:", request.method);
9697
console.log("Request received:", request.path);
9798
console.log("Request received:", request.data);
99+
100+
const response = await paystackClient.makeRequest(
101+
request.method,
102+
request.path,
103+
request.data
104+
)
105+
console.log("response: ", response)
98106

99107
return {
100108
content: [
101109
{
102110
type: "text",
103-
text: JSON.stringify({ "message": "API request processed successfully" }, null, 2),
111+
text: JSON.stringify(response, null, 2),
104112
mimeType: "application/json",
105113
},
106114
]
@@ -247,6 +255,7 @@ async function main() {
247255
console.error("Paystack MCP Server running on stdio...");
248256
}
249257

258+
250259
main().catch((error) => {
251260
console.error("Fatal error in main():", error);
252261
process.exit(1);

src/logger.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { config } from './config.js';
2+
// Define log levels
3+
export enum LogLevel {
4+
DEBUG = 'debug',
5+
INFO = 'info',
6+
WARN = 'warn',
7+
ERROR = 'error',
8+
}
9+
10+
// Map log levels to numeric values for comparison
11+
const logLevelValues: Record<LogLevel, number> = {
12+
[LogLevel.DEBUG]: 0,
13+
[LogLevel.INFO]: 1,
14+
[LogLevel.WARN]: 2,
15+
[LogLevel.ERROR]: 3,
16+
};
17+
18+
// Sensitive field patterns to redact
19+
const SENSITIVE_PATTERNS = [
20+
/authorization/i,
21+
/secret/i,
22+
/token/i,
23+
/api[_-]?key/i,
24+
/bearer/i,
25+
/credential/i,
26+
/secret[_-]?key/i,
27+
/cvv/i,
28+
/number/i,
29+
];
30+
/**
31+
* Redact sensitive fields in card objects
32+
* Only redacts cvv and number, keeps other fields visible
33+
*/
34+
function redactCardObject(card: any): any {
35+
if (Array.isArray(card)) {
36+
return card.map(redactCardObject);
37+
}
38+
39+
if (typeof card === 'object' && card !== null) {
40+
const redactedCard: any = {};
41+
for (const [key, value] of Object.entries(card)) {
42+
// Only redact cvv and number fields in card object
43+
if (key === 'cvv' || key === 'number') {
44+
redactedCard[key] = '[REDACTED]';
45+
} else {
46+
// Keep other card fields but recursively redact if they're objects
47+
redactedCard[key] = redactSensitiveData(value);
48+
}
49+
}
50+
return redactedCard;
51+
}
52+
53+
return card;
54+
}
55+
56+
57+
function redactSensitiveData(obj: any): any {
58+
if (obj === null || obj === undefined) {
59+
return obj;
60+
}
61+
62+
if (typeof obj === 'string') {
63+
// Redact bearer tokens and API keys in strings
64+
return obj.replace(/Bearer\s+\w+/gi, 'Bearer [REDACTED]')
65+
.replace(/sk_test_\w+/g, '[REDACTED_SECRET_KEY]');
66+
}
67+
68+
if (Array.isArray(obj)) {
69+
return obj.map(redactSensitiveData);
70+
}
71+
72+
if (typeof obj === 'object') {
73+
const redacted: any = {};
74+
for (const [key, value] of Object.entries(obj)) {
75+
// Special handling for card objects - only redact cvv and number
76+
if (key.toLowerCase() === 'card' && typeof value === 'object' && value !== null) {
77+
redacted[key] = redactCardObject(value);
78+
}
79+
// Check if key matches sensitive patterns
80+
else if (SENSITIVE_PATTERNS.some(pattern => pattern.test(key))) {
81+
redacted[key] = '[REDACTED]';
82+
} else {
83+
redacted[key] = redactSensitiveData(value);
84+
}
85+
}
86+
return redacted;
87+
}
88+
89+
return obj;
90+
}
91+
92+
class Logger {
93+
private currentLogLevel: LogLevel;
94+
95+
constructor() {
96+
this.currentLogLevel = config.LOG_LEVEL as LogLevel;
97+
}
98+
99+
private shouldLog(level: LogLevel): boolean {
100+
return logLevelValues[level] >= logLevelValues[this.currentLogLevel];
101+
}
102+
103+
private formatLog(level: LogLevel, message: string, meta?: any) {
104+
const timestamp = new Date().toISOString();
105+
const logEntry = {
106+
timestamp,
107+
level,
108+
message,
109+
...(meta && { meta: redactSensitiveData(meta) }),
110+
};
111+
112+
return JSON.stringify(logEntry);
113+
}
114+
115+
debug(message: string, meta?: any) {
116+
// Disabled for MCP stdio communication
117+
if (this.shouldLog(LogLevel.DEBUG)) {
118+
console.error(this.formatLog(LogLevel.DEBUG, message, meta));
119+
}
120+
}
121+
122+
info(message: string, meta?: any) {
123+
// Disabled for MCP stdio communication
124+
if (this.shouldLog(LogLevel.INFO)) {
125+
console.error(this.formatLog(LogLevel.INFO, message, meta));
126+
}
127+
}
128+
129+
warn(message: string, meta?: any) {
130+
// Disabled for MCP stdio communication
131+
if (this.shouldLog(LogLevel.WARN)) {
132+
console.error(this.formatLog(LogLevel.WARN, message, meta));
133+
}
134+
}
135+
136+
error(message: string, meta?: any) {
137+
// Disabled for MCP stdio communication
138+
if (this.shouldLog(LogLevel.ERROR)) {
139+
console.error(this.formatLog(LogLevel.ERROR, message, meta));
140+
}
141+
}
142+
143+
/**
144+
* Log API request
145+
*/
146+
logRequest(method: string, url: string, data?: any, headers?: any) {
147+
this.debug('API Request', {
148+
method,
149+
url,
150+
data,
151+
headers,
152+
});
153+
}
154+
155+
/**
156+
* Log API response
157+
*/
158+
logResponse(method: string, url: string, status: number, data?: any) {
159+
this.debug('API Response', {
160+
method,
161+
url,
162+
status,
163+
data,
164+
});
165+
}
166+
167+
/**
168+
* Log tool call
169+
*/
170+
logToolCall(toolName: string, params?: any) {
171+
this.info('Tool called', {
172+
tool: toolName,
173+
params,
174+
});
175+
}
176+
}
177+
178+
export const logger = new Logger();

src/openapi-parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ export class OpenAPIParser {
240240

241241

242242
getOperationById(operationId: string): Partial<Oas.Operation> | undefined {
243-
return this.operations[operationId];
243+
return this.operations[operationId];
244244
}
245245

246246
getOperations(): Record<string, Partial<Oas.Operation>> {

0 commit comments

Comments
 (0)