Skip to content

Commit 25aa2c7

Browse files
Merge pull request #8 from PaystackOSS/chore-tooling-refactor
Chore tooling refactor
2 parents 41754f5 + 1f3442b commit 25aa2c7

11 files changed

Lines changed: 528 additions & 361 deletions

package-lock.json

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

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,4 @@ See [contributing.md](contributing.md) for more details.
160160

161161
## License
162162

163-
MIT
163+
MIT

src/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ dotenv.config();
66

77
// Define schema for required environment variables
88
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."',
9+
PAYSTACK_TEST_SECRET_KEY: z.string().min(30, 'PAYSTACK_TEST_SECRET_KEY is required').refine(val => val.startsWith('sk_test_'), {
10+
message: 'PAYSTACK_TEST_SECRET_KEY must begin with "sk_test_. No live keys allowed."',
1111
}),
1212
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
1313
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
@@ -17,7 +17,7 @@ const envSchema = z.object({
1717
function validateEnv() {
1818
try {
1919
return envSchema.parse({
20-
PAYSTACK_SECRET_KEY_TEST: process.env.PAYSTACK_SECRET_KEY_TEST,
20+
PAYSTACK_TEST_SECRET_KEY: process.env.PAYSTACK_TEST_SECRET_KEY,
2121
NODE_ENV: process.env.NODE_ENV || 'development',
2222
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
2323
});
@@ -36,6 +36,6 @@ export const config = validateEnv();
3636
// Paystack API configuration
3737
export const paystackConfig = {
3838
baseURL: 'https://api.paystack.co',
39-
secretKey: config.PAYSTACK_SECRET_KEY_TEST,
39+
secretKey: config.PAYSTACK_TEST_SECRET_KEY,
4040
timeout: 30000, // 30 seconds
4141
} as const;

src/index.ts

Lines changed: 2 additions & 254 deletions
Original file line numberDiff line numberDiff line change
@@ -1,261 +1,9 @@
1-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3-
import path from "path";
4-
import * as z from "zod";
5-
import { OpenAPIParser } from "./openapi-parser";
6-
import { CreateMessageResultSchema } from "@modelcontextprotocol/sdk/types.js";
7-
import { paystackClient } from "./paystack-client";
8-
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";
11-
12-
// Create server instance
13-
const server = new McpServer({
14-
name: "paystack",
15-
version: "1.0.0",
16-
});
17-
18-
const oasPath = path.join(__dirname, "./", "data/paystack.openapi.yaml");
19-
const openapi = new OpenAPIParser(oasPath);
20-
21-
async function initializeServer() {
22-
// Parse OpenAPI spec before registering tools
23-
await openapi.parse();
24-
25-
server.registerTool(
26-
"get_paystack_operation",
27-
{
28-
description: "Get Paystack API operation details by operation ID",
29-
annotations: {
30-
title: "Get endpoint details by operation ID",
31-
},
32-
inputSchema: {
33-
operation_id: z
34-
.string()
35-
.describe("The operation ID of the Paystack API endpoint"),
36-
}
37-
},
38-
async ({ operation_id }) => {
39-
40-
try {
41-
const operation = openapi.getOperationById(operation_id.trim());
42-
console.error("Operation: ", operation)
43-
44-
if (!operation) {
45-
return {
46-
content: [
47-
{
48-
type: "text",
49-
text: `Operation with ID ${operation_id} not found.`,
50-
},
51-
]
52-
}
53-
}
54-
55-
return {
56-
content: [
57-
{
58-
type: "text",
59-
text: JSON.stringify(operation, null, 2),
60-
mimeType: "application/json",
61-
},
62-
]
63-
}
64-
} catch {
65-
return {
66-
content: [
67-
{
68-
type: "text",
69-
text: `Operation with ID ${operation_id} not found.`,
70-
},
71-
]
72-
}
73-
}
74-
}
75-
)
76-
77-
server.registerTool(
78-
"make_paystack_request",
79-
{
80-
description: `Make a Paystack API request using the details of the operation. Be sure
81-
to get all operation details including method, path path parameters, query parameters,
82-
and request body before making a call.`,
83-
annotations: {
84-
title: "Get endpoint details by operation ID",
85-
},
86-
inputSchema: {
87-
request: z.object({
88-
method: z.string().describe("HTTP method of the API request"),
89-
path: z.string().describe("Path of the API request"),
90-
data: z.looseObject({}).optional().describe("Request data"),
91-
})
92-
}
93-
},
94-
async ({ request }) => {
95-
try {
96-
console.error("Request received:", request.method);
97-
console.error("Request received:", request.path);
98-
console.error("Request received:", request.data);
99-
100-
const response = await paystackClient.makeRequest(
101-
request.method,
102-
request.path,
103-
request.data
104-
)
105-
console.error("response: ", response)
106-
107-
return {
108-
content: [
109-
{
110-
type: "text",
111-
text: JSON.stringify(response, null, 2),
112-
mimeType: "application/json",
113-
},
114-
]
115-
}
116-
} catch {
117-
return {
118-
content: [
119-
{
120-
type: "text",
121-
text: `Unable to make request.`,
122-
},
123-
]
124-
}
125-
}
126-
}
127-
)
128-
129-
server.registerTool(
130-
"get_operation_guided",
131-
{
132-
description: "Get Paystack API operation details from user input",
133-
annotations: {
134-
title: "Get endpoint details from user input",
135-
readOnlyHint: true,
136-
destructiveHint: false,
137-
idempotentHint: false,
138-
openWorldHint: false,
139-
},
140-
},
141-
async () => {
142-
const res = await server.server.request({
143-
method: "sampling/createMessage",
144-
params: {
145-
messages: [{
146-
role: "user",
147-
content: [
148-
{
149-
type: "text",
150-
text: `Review the OpenAPI specification and infer the operation ID of the
151-
Paystack API endpoint from the user input.
152-
For example if the user's input is: 'I want to create a new customer in Paystack.'
153-
review the OpenAPI spec and respond with the most logical operationId:
154-
which is 'customer_create'. Return just the operationId in your response.`,
155-
},
156-
],
157-
}],
158-
maxTokens: 1024,
159-
}
160-
}, CreateMessageResultSchema)
161-
162-
if (res.content.type !== "text") {
163-
return {
164-
content: [
165-
{
166-
type: "text",
167-
text: `Could not infer operation ID from user input.`,
168-
}
169-
]
170-
}
171-
}
172-
173-
try {
174-
const operation_id = res.content.text.trim();
175-
const operation = openapi.getOperationById(operation_id);
176-
177-
if (!operation) {
178-
return {
179-
content: [
180-
{
181-
type: "text",
182-
text: `Operation with ID ${operation_id} not found.`,
183-
},
184-
],
185-
isError: true,
186-
}
187-
}
188-
189-
return {
190-
content: [
191-
{
192-
type: "text",
193-
text: JSON.stringify(operation, null, 2),
194-
mimeType: "application/json",
195-
},
196-
]
197-
}
198-
} catch {
199-
return {
200-
content: [
201-
{
202-
type: "text",
203-
text: `Operation with ID cannot be infered.`,
204-
},
205-
]
206-
}
207-
}
208-
}
209-
)
210-
211-
server.registerResource(
212-
"operation-list",
213-
new ResourceTemplate("openapi://operations/list", { list: undefined }),
214-
{
215-
description: "Retrieve all operation IDs",
216-
title: "List of Paystack API operation IDs",
217-
mimeType: "text/plain",
218-
},
219-
async (uri) => {
220-
// await openapi.parse();
221-
const operations = openapi.getOperations();
222-
const operationIds = Object.keys(operations);
223-
224-
if (operationIds.length === 0) {
225-
return {
226-
contents: [
227-
{
228-
uri: uri.href,
229-
type: "text",
230-
text: "Unable to list operations.",
231-
mimeType: "text/plain",
232-
},
233-
]
234-
}
235-
}
236-
237-
return {
238-
contents: [
239-
{
240-
uri: uri.href,
241-
type: "text",
242-
text: operationIds.join("\n"),
243-
mimeType: "text/plain",
244-
},
245-
]
246-
}
247-
}
248-
)
249-
}
1+
import { startServer } from "./server";
2502

2513
async function main() {
252-
await initializeServer();
253-
const transport = new StdioServerTransport();
254-
await server.connect(transport);
255-
console.error("Paystack MCP Server running on stdio...");
4+
await startServer();
2565
}
2576

258-
2597
main().catch((error) => {
2608
console.error("Fatal error in main():", error);
2619
process.exit(1);

src/resources/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { OpenAPIParser } from "../openapi-parser";
3+
import { registerOperationListResource } from "./paystack-operation-list";
4+
5+
export function registerAllResources(
6+
server: McpServer,
7+
openapi: OpenAPIParser
8+
) {
9+
registerOperationListResource(server, openapi);
10+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { OpenAPIParser } from "../openapi-parser";
3+
4+
export function registerOperationListResource(
5+
server: McpServer,
6+
openapi: OpenAPIParser
7+
) {
8+
server.registerResource(
9+
"paystack_operation_list",
10+
"paystack://operations/list",
11+
{
12+
description: "Retrieve all Paystack API details",
13+
title: "Paystack API details",
14+
mimeType: "application/json",
15+
},
16+
async (uri) => {
17+
const operations = openapi.getOperations();
18+
19+
if (Object.keys(operations).length === 0) {
20+
return {
21+
contents: [
22+
{
23+
uri: uri.href,
24+
text: JSON.stringify({"message": "Unable to retrive all operations"}),
25+
mimeType: "application/json",
26+
},
27+
]
28+
}
29+
}
30+
return {
31+
contents: [{
32+
uri: uri.href,
33+
text: JSON.stringify(operations, null, 2),
34+
mimeType: "application/json"
35+
}]
36+
};
37+
}
38+
);
39+
}

src/server.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3+
import path from "path";
4+
import { OpenAPIParser } from "./openapi-parser";
5+
import { registerAllTools } from "./tools";
6+
import { registerAllResources } from "./resources";
7+
8+
async function createServer() {
9+
const server = new McpServer({
10+
name: "paystack",
11+
version: "1.0.0",
12+
});
13+
14+
const oasPath = path.join(__dirname, "./", "data/paystack.openapi.yaml");
15+
const openapi = new OpenAPIParser(oasPath);
16+
17+
await openapi.parse();
18+
19+
registerAllTools(server, openapi);
20+
registerAllResources(server, openapi);
21+
22+
return server;
23+
}
24+
25+
export async function startServer() {
26+
const server = await createServer();
27+
const transport = new StdioServerTransport();
28+
await server.connect(transport);
29+
console.error("Paystack MCP Server running on stdio...");
30+
return server;
31+
}

0 commit comments

Comments
 (0)