From 831dd8435193fbc1bd87ec56b908a5e1f5d565f1 Mon Sep 17 00:00:00 2001 From: Damilola Odujoko Date: Mon, 26 Jan 2026 10:22:58 +0100 Subject: [PATCH] feat: implement the core api request tool functionality --- package-lock.json | 35 +++++- package.json | 4 +- src/index.ts | 310 +++++++++++++++++++++++++++------------------- 3 files changed, 213 insertions(+), 136 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac6f495..772f742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@modelcontextprotocol/inspector": "^0.18.0", "@modelcontextprotocol/sdk": "^1.25.2", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "bin": { "paystack": "build/index.js" @@ -728,6 +728,15 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/inspector-client/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@modelcontextprotocol/inspector-server": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-server/-/inspector-server-0.18.0.tgz", @@ -746,6 +755,24 @@ "mcp-inspector-server": "build/index.js" } }, + "node_modules/@modelcontextprotocol/inspector-server/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/inspector/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.25.3", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", @@ -4845,9 +4872,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index c3a50f5..824f5c3 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "paystack": "./build/index.js" }, "scripts": { - "build": "tsc", + "build": "tsc && cp -r src/data build/", "build:watch": "tsc --watch", "dev": "tsx src/index.ts", "inspect": "set DANGEROUSLY_OMIT_AUTH=true && CLIENT_PORT=8090 SERVER_PORT=9000 npx @modelcontextprotocol/inspector npm run dev", @@ -21,7 +21,7 @@ "dependencies": { "@modelcontextprotocol/inspector": "^0.18.0", "@modelcontextprotocol/sdk": "^1.25.2", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "@apidevtools/swagger-parser": "^12.1.0", diff --git a/src/index.ts b/src/index.ts index 17e9f85..8e600da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import path from "path"; -import { z } from "zod"; +import * as z from "zod"; import { OpenAPIParser } from "./openapi-parser"; import { CreateMessageResultSchema } from "@modelcontextprotocol/sdk/types.js"; @@ -17,26 +17,50 @@ const server = new McpServer({ const oasPath = path.join(__dirname, "./", "data/paystack.openapi.yaml"); const openapi = new OpenAPIParser(oasPath); -// Temporarily ignore ts error on Zod schema for get_operation method -// @ts-ignore -server.registerTool( - "get_operation", - { - description: "Get Paystack API operation details by operation ID", - annotations: { - title: "Get endpoint details by operation ID", +async function initializeServer() { + // Parse OpenAPI spec before registering tools + await openapi.parse(); + + server.registerTool( + "get_paystack_operation", + { + description: "Get Paystack API operation details by operation ID", + annotations: { + title: "Get endpoint details by operation ID", + }, + inputSchema: { + operation_id: z + .string() + .describe("The operation ID of the Paystack API endpoint"), + } }, - inputSchema: { - operation_id: z - .string() - .describe("The operation ID of the Paystack API endpoint"), - } - }, - async ({ operation_id }) => { - try { - const operation = openapi.getOperationById(operation_id); + async ({ operation_id }) => { + + try { + const operation = openapi.getOperationById(operation_id.trim()); + console.log("Operation: ", operation) + + if (!operation) { + return { + content: [ + { + type: "text", + text: `Operation with ID ${operation_id} not found.`, + }, + ] + } + } - if (!operation) { + return { + content: [ + { + type: "text", + text: JSON.stringify(operation, null, 2), + mimeType: "application/json", + }, + ] + } + } catch { return { content: [ { @@ -46,152 +70,178 @@ server.registerTool( ] } } - - return { - content: [ - { - type: "text", - text: JSON.stringify(operation, null, 2), - mimeType: "application/json", - }, - ] - } - } catch { - return { - content: [ - { - type: "text", - text: `Operation with ID ${operation_id} not found.`, - }, - ] - } } - } -) - -server.registerTool( - "get_operation_guided", - { - description: "Get Paystack API operation details from user input", - annotations: { - title: "Get endpoint details from user input", - readOnlyHint: true, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, + ) + + server.registerTool( + "make_paystack_request", + { + description: `Make a Paystack API request using the details of the operation. Be sure + to get all operation details including method, path path parameters, query parameters, + and request body before making a call.`, + annotations: { + title: "Get endpoint details by operation ID", + }, + inputSchema: { + request: z.object({ + method: z.string().describe("HTTP method of the API request"), + path: z.string().describe("Path of the API request"), + data: z.looseObject({}).optional().describe("Request data"), + }) + } }, - }, - async () => { - const res = await server.server.request({ - method: "sampling/createMessage", - params: { - messages: [{ - role: "user", + async ({ request }) => { + try { + console.log("Request received:", request.method); + console.log("Request received:", request.path); + console.log("Request received:", request.data); + + return { + content: [ + { + type: "text", + text: JSON.stringify({ "message": "API request processed successfully" }, null, 2), + mimeType: "application/json", + }, + ] + } + } catch { + return { content: [ { type: "text", - text: `Review the OpenAPI specification and infer the operation ID of the + text: `Unable to make request.`, + }, + ] + } + } + } + ) + + server.registerTool( + "get_operation_guided", + { + description: "Get Paystack API operation details from user input", + annotations: { + title: "Get endpoint details from user input", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + async () => { + const res = await server.server.request({ + method: "sampling/createMessage", + params: { + messages: [{ + role: "user", + content: [ + { + type: "text", + text: `Review the OpenAPI specification and infer the operation ID of the Paystack API endpoint from the user input. For example if the user's input is: 'I want to create a new customer in Paystack.' review the OpenAPI spec and respond with the most logical operationId: which is 'customer_create'. Return just the operationId in your response.`, - }, - ], - }], - maxTokens: 1024, + }, + ], + }], + maxTokens: 1024, + } + }, CreateMessageResultSchema) + + if (res.content.type !== "text") { + return { + content: [ + { + type: "text", + text: `Could not infer operation ID from user input.`, + } + ] + } } - }, CreateMessageResultSchema) - if(res.content.type !== "text") { - return { - content: [ - { - type: "text", - text: `Could not infer operation ID from user input.`, + try { + const operation_id = res.content.text.trim(); + const operation = openapi.getOperationById(operation_id); + + if (!operation) { + return { + content: [ + { + type: "text", + text: `Operation with ID ${operation_id} not found.`, + }, + ], + isError: true, } - ] + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(operation, null, 2), + mimeType: "application/json", + }, + ] + } + } catch { + return { + content: [ + { + type: "text", + text: `Operation with ID cannot be infered.`, + }, + ] + } } } + ) - try { - const operation_id = res.content.text.trim(); - const operation = openapi.getOperationById(operation_id); + server.registerResource( + "operation-list", + new ResourceTemplate("openapi://operations/list", { list: undefined }), + { + description: "Retrieve all operation IDs", + title: "List of Paystack API operation IDs", + mimeType: "text/plain", + }, + async (uri) => { + // await openapi.parse(); + const operations = openapi.getOperations(); + const operationIds = Object.keys(operations); - if (!operation) { + if (operationIds.length === 0) { return { - content: [ + contents: [ { + uri: uri.href, type: "text", - text: `Operation with ID ${operation_id} not found.`, + text: "Unable to list operations.", + mimeType: "text/plain", }, - ], - isError: true, + ] } } - return { - content: [ - { - type: "text", - text: JSON.stringify(operation, null, 2), - mimeType: "application/json", - }, - ] - } - } catch { - return { - content: [ - { - type: "text", - text: `Operation with ID cannot be infered.`, - }, - ] - } - } - } -) - -server.registerResource( - "operation-list", - new ResourceTemplate("openapi://operations/list", {list: undefined}), - { - description: "Retrieve all operation IDs", - title: "List of Paystack API operation IDs", - mimeType: "text/plain", - }, - async (uri) => { - // await openapi.parse(); - const operations = openapi.getOperations(); - const operationIds = Object.keys(operations); - - if (operationIds.length === 0) { return { contents: [ { uri: uri.href, type: "text", - text: "Unable to list operations.", + text: operationIds.join("\n"), mimeType: "text/plain", }, ] } } - - return { - contents: [ - { - uri: uri.href, - type: "text", - text: operationIds.join("\n"), - mimeType: "text/plain", - }, - ] - } - } -) + ) +} async function main() { - await openapi.parse(); + await initializeServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Paystack MCP Server running on stdio...");