Skip to content

Commit cf0f6a5

Browse files
Merge pull request #29 from PaystackOSS/feat/docs-skills
Feat/docs skills
2 parents b49dc69 + 145a205 commit cf0f6a5

9 files changed

Lines changed: 286 additions & 6 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ node_modules/
3838
!.env.example
3939

4040
# Stores VSCode versions used for testing VSCode extensions
41-
.vscode-test
41+
.vscode-test
42+
.vscode

.vscode/mcp.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"servers": {
33
"test-mcp-server": {
44
"type": "stdio",
5-
"command": "node",
5+
"command": "/Users/andrew/.nvm/versions/node/v22.12.0/bin/node",
66
"args": [
77
"build/index.js"
88
],
@@ -13,7 +13,9 @@
1313
"type": "node"
1414
}
1515
},
16-
"env": {}
16+
"env": {
17+
"PAYSTACK_TEST_SECRET_KEY": "sk_test_23bb8447ce48d687dfbd9017afcc63321ef1cde9"
18+
}
1719
}
1820
}
1921
}

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,13 @@ When you ask your AI assistant something like _"Get me the last 5 transactions o
135135
136136
### Prompt recommendation
137137
138-
To get the best results when using this MCP server, be specific in your prompts and always include "Paystack" in your requests. This helps the LLM quickly identify and use the appropriate Paystack tools.
138+
To get the best results when using this MCP server, be specific in your prompts and always include "Paystack" in your requests. The server provides built-in instructions and a knowledge resource (`paystack://skill`) that help the AI assistant find the right documentation, code snippets, and API details.
139139
140140
**Good prompts:**
141141
- "Initialize a Paystack transaction for 50000 NGN"
142142
- "Create a customer with email user@example.com on my Paystack account"
143143
- "How can I send money with the Paystack API?"
144+
- "Show me a cURL example for verifying a Paystack transaction"
144145
145146
**Less effective prompts:**
146147
- "List my transactions" (unclear which service to use)

src/data/paystack-skill.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
name: paystack-skill
3+
description: Guide users through using the Paystack's products and APIs. Use this when users ask for sample code, guides, example usage, references, webhook implementation or similar structured content. Trigger when you're not certain based on information from existing resources.
4+
---
5+
6+
## Documentation Index
7+
8+
For integration guides, best practices, and detailed documentation, use the Paystack LLM-friendly docs index:
9+
https://paystack.com/docs/llms.txt
10+
11+
This index covers payments, transfers, terminal, guides, libraries, API reference, and API changelog.
12+
13+
## Code Snippets
14+
15+
Code snippets for Paystack API endpoints and integration guides are maintained in the PaystackOSS/doc-code-snippets repository. Snippets are available in JavaScript (Node.js), Shell/cURL, and PHP.
16+
17+
### Browsing Snippets
18+
- API Reference snippets: https://github.com/PaystackOSS/doc-code-snippets/tree/main/src/api
19+
- Documentation snippets: https://github.com/PaystackOSS/doc-code-snippets/tree/main/src/doc
20+
21+
### Fetching a Specific Snippet
22+
Once you know the exact path from browsing, fetch the raw content at:
23+
https://raw.githubusercontent.com/PaystackOSS/doc-code-snippets/main/src/api/{topic}/{action}/index.{js,sh}
24+
25+
For example:
26+
- https://raw.githubusercontent.com/PaystackOSS/doc-code-snippets/main/src/api/transactions/initialize/index.js
27+
- https://raw.githubusercontent.com/PaystackOSS/doc-code-snippets/main/src/api/transactions/initialize/index.sh
28+
29+
### Snippet Notes
30+
- Snippets use placeholder values like "SECRET_KEY" or "YOUR_SECRET_KEY" — replace with actual test keys
31+
- Not every endpoint has a snippet; if unavailable, construct the request from the operation details provided by the "get_paystack_operation" tool
32+
- The supported webhook events are in https://raw.githubusercontent.com/PaystackOSS/doc-code-snippets/main/dist/doc/payments/webhooks/events.js
33+
34+
## Payment Channels by Country
35+
36+
| Country | Currencies | Payment Channels |
37+
|---------|----------|-----------------|
38+
| Nigeria | NGN, USD | Cards (Visa, Mastercard, Verve, Amex), Bank Transfer, USSD, QR Code, Apple Pay |
39+
| Ghana | GHS | Cards (Visa, Mastercard, Verve), Mobile Money (MTN, AirtelTigo, Telecel), Bank Transfer, QR Code |
40+
| South Africa | ZAR | Cards (Visa, Mastercard, Verve, Amex), Apple Pay, Scan to Pay, Capitec Pay, Ozow |
41+
| Kenya | KES, USD | Cards (Visa, Mastercard, Verve), Mobile Money (M-PESA, Airtel Money), Bank Transfers (Pesalink)
42+
| Côte d'Ivoire | XOF | Cards (Visa, Mastercard, Verve), Mobile Money (MTN MoMo, Wave, Orange Money), Apple Pay |

src/resources/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { OpenAPIParser } from "../openapi-parser";
33
import { registerOperationListResource } from "./paystack-operation-list";
4+
import { registerSkillResource } from "./paystack-skill";
45

56
export function registerAllResources(
67
server: McpServer,
7-
openapi: OpenAPIParser
8+
openapi: OpenAPIParser,
9+
skillContent: string
810
) {
911
registerOperationListResource(server, openapi);
12+
registerSkillResource(server, skillContent);
1013
}

src/resources/paystack-skill.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
3+
export function registerSkillResource(server: McpServer, skillContent: string) {
4+
server.registerResource(
5+
"paystack_skill",
6+
"paystack://skill",
7+
{
8+
description:
9+
"Paystack developer knowledge: docs index pointer, code snippet URL patterns, and payment channel reference",
10+
title: "Paystack Developer Knowledge",
11+
mimeType: "text/markdown",
12+
},
13+
async (uri) => {
14+
return {
15+
contents: [
16+
{
17+
uri: uri.href,
18+
text: skillContent,
19+
mimeType: "text/markdown",
20+
},
21+
],
22+
};
23+
}
24+
);
25+
}

src/server.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,49 @@ import path from "path";
44
import { OpenAPIParser } from "./openapi-parser";
55
import { registerAllTools } from "./tools";
66
import { registerAllResources } from "./resources";
7+
import { loadSkillContent } from "./skill-loader";
8+
9+
const SERVER_INSTRUCTIONS = `You are connected to the Paystack MCP Server, which provides access to the full Paystack API.
10+
11+
## Source Priority
12+
1. Use this server's tools and resources as the primary source of truth for API details:
13+
- "paystack_operation_list" resource: browse all available API operations
14+
- "get_paystack_operation" tool: get endpoint details (method, path, parameters, request body)
15+
- "make_paystack_request" tool: execute API requests
16+
2. For integration guides, best practices, and deeper context, refer to Paystack documentation at https://paystack.com/docs/llms.txt
17+
3. For code snippets in JS/TS or cURL, refer to the "paystack_skill" resource
18+
4. If information is not available from the above sources, say so clearly — do not invent Paystack-specific details
19+
20+
## Critical Accuracy Rules
21+
- All amounts must be in the smallest currency unit: kobo (NGN), pesewas (GHS), cents (ZAR/KES/USD). XOF has no subunit but amounts must still be multiplied by 100.
22+
- API requests require authentication: secret keys (server-side) or public keys (client-side only for Popup/Mobile SDKs)
23+
- This server only accepts test keys (sk_test_*). Never use live keys.
24+
- Prefer to use Webhooks for event-driven flows based on your paystack-skills file.
25+
- Always verify transactions server-side before delivering value
26+
- Validate webhook signatures using your secret key before processing events
27+
28+
## Workflow
29+
Always call "get_paystack_operation" to get endpoint details before calling "make_paystack_request". Do not guess endpoint paths, methods, or parameter names.
30+
`;
731

832
async function createServer(cliApiKey?: string) {
933
const server = new McpServer({
1034
name: "paystack",
1135
version: "0.0.1",
36+
}, {
37+
instructions: SERVER_INSTRUCTIONS,
1238
});
1339

1440
const oasPath = path.join(__dirname, "./", "data/paystack.openapi.yaml");
1541
const openapi = new OpenAPIParser(oasPath);
1642

1743
await openapi.parse();
1844

45+
const bundledSkillPath = path.join(__dirname, "data", "paystack-skill.md");
46+
const skillContent = await loadSkillContent(bundledSkillPath);
47+
1948
registerAllTools(server, openapi, cliApiKey);
20-
registerAllResources(server, openapi);
49+
registerAllResources(server, openapi, skillContent);
2150

2251
return server;
2352
}

src/skill-loader.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as fs from "node:fs/promises";
2+
import * as path from "node:path";
3+
import { homedir } from "node:os";
4+
5+
const DEFAULT_SKILL_URL =
6+
"https://raw.githubusercontent.com/PaystackOSS/paystack-mcp-server/main/src/data/paystack-skill.md";
7+
8+
const CACHE_DIR = path.join(homedir(), ".paystack-mcp");
9+
const CACHE_PATH = path.join(CACHE_DIR, "skill-cache.md");
10+
const MAX_AGE_MS = 48 * 60 * 60 * 1000; // 48 hours
11+
12+
export async function loadSkillContent(bundledPath: string): Promise<string> {
13+
const url = process.env.PAYSTACK_SKILL_URL || DEFAULT_SKILL_URL;
14+
15+
try {
16+
// Check disk cache freshness
17+
const stat = await fs.stat(CACHE_PATH);
18+
if (Date.now() - stat.mtimeMs < MAX_AGE_MS) {
19+
return await fs.readFile(CACHE_PATH, "utf-8");
20+
}
21+
} catch {
22+
// Cache missing or unreadable — continue to fetch
23+
}
24+
25+
try {
26+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
27+
if (res.ok) {
28+
const text = await res.text();
29+
if (text.includes("name: paystack")) {
30+
try {
31+
await fs.mkdir(CACHE_DIR, { recursive: true });
32+
await fs.writeFile(CACHE_PATH, text, "utf-8");
33+
} catch {
34+
// Cache write failed — non-fatal
35+
}
36+
return text;
37+
}
38+
}
39+
} catch {
40+
// Fetch failed or timed out — fall through
41+
}
42+
43+
// Fallback: stale cache → bundled file
44+
try {
45+
return await fs.readFile(CACHE_PATH, "utf-8");
46+
} catch {
47+
return await fs.readFile(bundledPath, "utf-8");
48+
}
49+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import assert from "node:assert";
2+
import * as fs from "node:fs";
3+
import * as path from "node:path";
4+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5+
import { registerSkillResource } from "../src/resources/paystack-skill.js";
6+
7+
const SKILL_CONTENT = fs.readFileSync(
8+
path.join(__dirname, "..", "src", "data", "paystack-skill.md"),
9+
"utf-8"
10+
);
11+
12+
describe("PaystackSkillResource", () => {
13+
let resourceHandler: any;
14+
let registeredName: string;
15+
let registeredUri: string;
16+
let registeredMetadata: any;
17+
18+
before(() => {
19+
const server = {
20+
registerResource: (name: string, uri: string, metadata: any, handler: any) => {
21+
registeredName = name;
22+
registeredUri = uri;
23+
registeredMetadata = metadata;
24+
resourceHandler = handler;
25+
}
26+
} as any;
27+
28+
registerSkillResource(server, SKILL_CONTENT);
29+
});
30+
31+
describe("Registration", () => {
32+
it("should register with the correct name", () => {
33+
assert.strictEqual(registeredName, "paystack_skill");
34+
});
35+
36+
it("should register with the correct URI", () => {
37+
assert.strictEqual(registeredUri, "paystack://skill");
38+
});
39+
40+
it("should set mimeType to text/markdown", () => {
41+
assert.strictEqual(registeredMetadata.mimeType, "text/markdown");
42+
});
43+
44+
it("should have a description", () => {
45+
assert.ok(registeredMetadata.description);
46+
assert.ok(registeredMetadata.description.length > 0);
47+
});
48+
});
49+
50+
describe("Content", () => {
51+
let content: string;
52+
53+
before(async () => {
54+
const mockUri = new URL("paystack://skill");
55+
const result = await resourceHandler(mockUri);
56+
content = result.contents[0].text;
57+
});
58+
59+
it("should return text/markdown mimeType in response", async () => {
60+
const mockUri = new URL("paystack://skill");
61+
const result = await resourceHandler(mockUri);
62+
assert.strictEqual(result.contents[0].mimeType, "text/markdown");
63+
});
64+
65+
it("should include documentation index section", () => {
66+
assert.ok(content.includes("## Documentation Index"));
67+
});
68+
69+
it("should include code snippets section", () => {
70+
assert.ok(content.includes("## Code Snippets"));
71+
});
72+
73+
it("should include payment channels by country section", () => {
74+
assert.ok(content.includes("## Payment Channels by Country"));
75+
});
76+
77+
it("should include links to Paystack docs", () => {
78+
assert.ok(content.includes("https://paystack.com/docs/llms.txt"));
79+
});
80+
81+
it("should include snippet repo URL pattern for JS", () => {
82+
assert.ok(content.includes("PaystackOSS/doc-code-snippets"));
83+
assert.ok(content.includes("index.js"));
84+
});
85+
86+
it("should include snippet repo URL pattern for Shell", () => {
87+
assert.ok(content.includes("index.sh"));
88+
});
89+
90+
it("should include all supported countries", () => {
91+
assert.ok(content.includes("Nigeria"));
92+
assert.ok(content.includes("Ghana"));
93+
assert.ok(content.includes("South Africa"));
94+
assert.ok(content.includes("Kenya"));
95+
assert.ok(content.includes("Côte d'Ivoire"));
96+
});
97+
});
98+
});
99+
100+
describe("ServerInstructions", () => {
101+
it("should pass instructions to the McpServer constructor", async () => {
102+
// Dynamically import server module to verify instructions are set
103+
// We check the source directly since McpServer options are private
104+
const fs = await import("node:fs");
105+
const path = await import("node:path");
106+
const serverSource = fs.readFileSync(
107+
path.join(__dirname, "../src/server.ts"),
108+
"utf-8"
109+
);
110+
111+
assert.ok(
112+
serverSource.includes("instructions:"),
113+
"server.ts should pass instructions to McpServer"
114+
);
115+
assert.ok(
116+
serverSource.includes("smallest currency unit"),
117+
"instructions should include currency unit rule"
118+
);
119+
assert.ok(
120+
serverSource.includes("get_paystack_operation"),
121+
"instructions should reference the operation tool"
122+
);
123+
assert.ok(
124+
serverSource.includes("do not invent"),
125+
"instructions should include anti-hallucination directive"
126+
);
127+
});
128+
});

0 commit comments

Comments
 (0)