diff --git a/README.md b/README.md index b476b7d..1626bac 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The toolkit provides the following tools for agents to use: * `get-documentation-section-content`: Retrieves the complete content for a specific documentation section. * `get-documentation-page`: Retrieves the complete content of a specific documentation page. * `get-oauth10a-integration-guide`: Retrieves the comprehensive OAuth 1.0a integration guide. +* `get-oauth20-integration-guide`: Retrieves the comprehensive OAuth 2.0 integration guide. * `get-openfinance-integration-guide`: Retrieves the comprehensive Open Finance integration guide. ### API Operations diff --git a/modelcontextprotocol/README.md b/modelcontextprotocol/README.md index ec22f63..5088cf3 100644 --- a/modelcontextprotocol/README.md +++ b/modelcontextprotocol/README.md @@ -14,7 +14,7 @@ This MCP server acts as a bridge between MCP clients and Mastercard Developers r - List available Mastercard services - Query API specifications and their operations - Access documentation for each service -- Retrieve integration guides for OAuth 1.0a and Open Finance +- Retrieve integration guides for OAuth 1.0a, OAuth 2.0, and Open Finance ## Available Tools @@ -40,6 +40,8 @@ This MCP server acts as a bridge between MCP clients and Mastercard Developers r - **`get-oauth10a-integration-guide`**: Retrieves the comprehensive OAuth 1.0a integration guide including step-by-step instructions, code examples, and best practices for Mastercard APIs. +- **`get-oauth20-integration-guide`**: Retrieves the comprehensive OAuth 2.0 integration guide including step-by-step instructions, code examples, and best practices for Mastercard APIs. + - **`get-openfinance-integration-guide`**: Retrieves the comprehensive Open Finance integration guide including setup instructions, API usage examples, and implementation best practices. ## Configuration Options diff --git a/typescript/package-lock.json b/typescript/package-lock.json index 3d7c9e1..6e56215 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -1,6 +1,6 @@ { "name": "@mastercard/developers-agent-toolkit", - "version": "0.1.4", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/typescript/package.json b/typescript/package.json index 59cd184..5c5c0b7 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.4", + "version": "0.1.5", "name": "@mastercard/developers-agent-toolkit", "homepage": "https://github.com/mastercard/developers-agent-toolkit", "description": "Agent Toolkit for Mastercard Developers Platform", diff --git a/typescript/src/modelcontextprotocol/__tests__/index.test.ts b/typescript/src/modelcontextprotocol/__tests__/index.test.ts index d884d51..ce0937f 100644 --- a/typescript/src/modelcontextprotocol/__tests__/index.test.ts +++ b/typescript/src/modelcontextprotocol/__tests__/index.test.ts @@ -38,7 +38,7 @@ describe('MastercardDevelopersAgentToolkit', () => { it('should list all tools with correct name/description when no config', async () => { const registeredTools = await listRegisteredTools({}); - const expectedTools = tools({}); + const expectedTools = tools(buildContext({})); expect(registeredTools).toHaveLength(expectedTools.length); expectedTools.forEach((expectedTool, index) => { @@ -60,10 +60,12 @@ describe('MastercardDevelopersAgentToolkit', () => { 'https://static.developer.mastercard.com/content/service/swagger/path.yaml', }); - const expectedTools = tools({ - serviceId: 'service', - apiSpecificationPath: '/service/swagger/path.yaml', - }).filter((tool) => tool.name !== 'get-services-list'); + const expectedTools = tools( + buildContext({ + apiSpecification: + 'https://static.developer.mastercard.com/content/service/swagger/path.yaml', + }) + ).filter((tool) => tool.name !== 'get-services-list'); expect(registeredTools).toHaveLength(expectedTools.length); expectedTools.forEach((expectedTool, index) => { @@ -84,9 +86,11 @@ describe('MastercardDevelopersAgentToolkit', () => { service: 'https://developer.mastercard.com/test-service/documentation/', }); - const expectedTools = tools({ serviceId: 'test-service' }).filter( - (tool) => tool.name !== 'get-services-list' - ); + const expectedTools = tools( + buildContext({ + service: 'https://developer.mastercard.com/test-service/documentation/', + }) + ).filter((tool) => tool.name !== 'get-services-list'); const registeredNames = registeredTools.map((tool) => tool.name); expect(registeredNames).not.toContain('get-services-list'); @@ -110,7 +114,7 @@ describe('buildContext function', () => { describe('success cases', () => { it('should return empty context when no config provided', () => { const result = buildContext({}); - expect(result).toEqual({}); + expect(result).toEqual(expect.objectContaining({})); }); it('should parse service URL and extract serviceId', () => { @@ -118,9 +122,11 @@ describe('buildContext function', () => { service: 'https://developer.mastercard.com/open-finance-us/documentation/', }); - expect(result).toEqual({ - serviceId: 'open-finance-us', - }); + expect(result).toEqual( + expect.objectContaining({ + serviceId: 'open-finance-us', + }) + ); }); it('should parse API specification URL and extract serviceId and apiSpecificationPath', () => { @@ -128,10 +134,12 @@ describe('buildContext function', () => { apiSpecification: 'https://static.developer.mastercard.com/content/test-service/swagger/api.yaml', }); - expect(result).toEqual({ - serviceId: 'test-service', - apiSpecificationPath: '/test-service/swagger/api.yaml', - }); + expect(result).toEqual( + expect.objectContaining({ + serviceId: 'test-service', + apiSpecificationPath: '/test-service/swagger/api.yaml', + }) + ); }); it('should handle nested API specification paths', () => { @@ -139,10 +147,12 @@ describe('buildContext function', () => { apiSpecification: 'https://static.developer.mastercard.com/content/payment-gateway/swagger/nested/spec.yaml', }); - expect(result).toEqual({ - serviceId: 'payment-gateway', - apiSpecificationPath: '/payment-gateway/swagger/nested/spec.yaml', - }); + expect(result).toEqual( + expect.objectContaining({ + serviceId: 'payment-gateway', + apiSpecificationPath: '/payment-gateway/swagger/nested/spec.yaml', + }) + ); }); it('should handle service IDs with hyphens and numbers', () => { @@ -150,9 +160,11 @@ describe('buildContext function', () => { service: 'https://developer.mastercard.com/open-finance-us-v2/documentation/', }); - expect(result).toEqual({ - serviceId: 'open-finance-us-v2', - }); + expect(result).toEqual( + expect.objectContaining({ + serviceId: 'open-finance-us-v2', + }) + ); }); }); diff --git a/typescript/src/modelcontextprotocol/index.ts b/typescript/src/modelcontextprotocol/index.ts index 024e6ef..122983a 100644 --- a/typescript/src/modelcontextprotocol/index.ts +++ b/typescript/src/modelcontextprotocol/index.ts @@ -1,8 +1,12 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { defaultDevelopersApi } from '@/shared/api'; import { tools } from '@/shared/tools'; import { ToolContext } from '@/shared/types'; import { version } from '../../package.json'; +export type { DevelopersApi, Tool, ToolContext } from '@/shared/types'; +export { tools } from '@/shared/tools'; + export interface MastercardDevelopersAgentToolkitConfig { service?: string; apiSpecification?: string; @@ -61,7 +65,7 @@ export class MastercardDevelopersAgentToolkit extends McpServer { export function buildContext( config: MastercardDevelopersAgentToolkitConfig ): ToolContext { - const context: ToolContext = {}; + const context: ToolContext = { client: defaultDevelopersApi }; if (config.service != null) { const serviceId = parseServiceIdFromUrl(config.service); if (serviceId == null) { diff --git a/typescript/src/shared/api/__tests__/index.test.ts b/typescript/src/shared/api/__tests__/index.test.ts index 08fb8bf..93f02dc 100644 --- a/typescript/src/shared/api/__tests__/index.test.ts +++ b/typescript/src/shared/api/__tests__/index.test.ts @@ -1,4 +1,4 @@ -import { MastercardAPIClient } from '@/shared/api'; +import { defaultDevelopersApi, MastercardAPIClient } from '@/shared/api'; import fetch, { RequestInfo, Response } from 'node-fetch'; const mcd = (path: string) => { @@ -8,6 +8,12 @@ const mcd = (path: string) => { const mockFetch = fetch as jest.MockedFunction; jest.mock('node-fetch'); +describe('defaultDevelopersApi', () => { + it('uses the default MastercardAPIClient implementation', () => { + expect(defaultDevelopersApi).toBeInstanceOf(MastercardAPIClient); + }); +}); + describe('MastercardAPIClient', () => { let client: MastercardAPIClient; diff --git a/typescript/src/shared/api/index.ts b/typescript/src/shared/api/index.ts index 7e56035..4b39da7 100644 --- a/typescript/src/shared/api/index.ts +++ b/typescript/src/shared/api/index.ts @@ -1,6 +1,8 @@ import { z } from 'zod'; import fetch from 'node-fetch'; +import type { DevelopersApi } from '@/shared/types'; + const PathSchema = z .string() .min(1, 'Path must be a non-empty string') @@ -106,5 +108,4 @@ export class MastercardAPIClient { } } -const api = new MastercardAPIClient(); -export default api; +export const defaultDevelopersApi: DevelopersApi = new MastercardAPIClient(); diff --git a/typescript/src/shared/tools/documentation/__tests__/getDocumentation.test.ts b/typescript/src/shared/tools/documentation/__tests__/getDocumentation.test.ts index 43daa80..f4d8ab0 100644 --- a/typescript/src/shared/tools/documentation/__tests__/getDocumentation.test.ts +++ b/typescript/src/shared/tools/documentation/__tests__/getDocumentation.test.ts @@ -2,11 +2,9 @@ import { execute, getParameters, } from '@/shared/tools/documentation/getDocumentation'; -import api from '@/shared/api'; +import { createMockApi } from '@/tests/mockDevelopersApi'; -jest.mock('@/shared/api'); - -const mockApi = api as jest.Mocked; +const mockApi = createMockApi(); describe('execute', () => { beforeEach(() => { @@ -17,7 +15,10 @@ describe('execute', () => { const mockResult = 'mock documentation'; mockApi.getDocumentation.mockResolvedValue(mockResult); - const result = await execute({}, { serviceId: 'test-service' }); + const result = await execute( + { client: mockApi }, + { serviceId: 'test-service' } + ); expect(mockApi.getDocumentation).toHaveBeenCalledWith('test-service'); expect(result).toBe(mockResult); @@ -27,7 +28,10 @@ describe('execute', () => { const mockResult = 'mock documentation'; mockApi.getDocumentation.mockResolvedValue(mockResult); - const result = await execute({ serviceId: 'context-service' }, {}); + const result = await execute( + { client: mockApi, serviceId: 'context-service' }, + {} + ); expect(mockApi.getDocumentation).toHaveBeenCalledWith('context-service'); expect(result).toBe(mockResult); @@ -36,7 +40,7 @@ describe('execute', () => { describe('getParameters', () => { it('should return the correct parameters if no context', () => { - const parameters = getParameters({}); + const parameters = getParameters({ client: mockApi }); const fields = Object.keys(parameters.shape); expect(fields).toEqual(['serviceId']); @@ -44,7 +48,10 @@ describe('getParameters', () => { }); it('should return the correct parameters if serviceId is specified in context', () => { - const parameters = getParameters({ serviceId: 'test-service' }); + const parameters = getParameters({ + client: mockApi, + serviceId: 'test-service', + }); const fields = Object.keys(parameters.shape); expect(fields).toEqual([]); diff --git a/typescript/src/shared/tools/documentation/__tests__/getDocumentationPage.test.ts b/typescript/src/shared/tools/documentation/__tests__/getDocumentationPage.test.ts index b79b542..6eb0a30 100644 --- a/typescript/src/shared/tools/documentation/__tests__/getDocumentationPage.test.ts +++ b/typescript/src/shared/tools/documentation/__tests__/getDocumentationPage.test.ts @@ -2,11 +2,9 @@ import { execute, getParameters, } from '@/shared/tools/documentation/getDocumentationPage'; -import api from '@/shared/api'; +import { createMockApi } from '@/tests/mockDevelopersApi'; -jest.mock('@/shared/api'); - -const mockApi = api as jest.Mocked; +const mockApi = createMockApi(); describe('execute', () => { beforeEach(() => { @@ -17,7 +15,10 @@ describe('execute', () => { const mockResult = 'mock documentation page content'; mockApi.getDocumentationPage.mockResolvedValue(mockResult); - const result = await execute({}, { pagePath: '/test/page.md' }); + const result = await execute( + { client: mockApi }, + { pagePath: '/test/page.md' } + ); expect(mockApi.getDocumentationPage).toHaveBeenCalledWith('/test/page.md'); expect(result).toBe(mockResult); @@ -26,7 +27,7 @@ describe('execute', () => { describe('getParameters', () => { it('should return the correct parameters if no context', () => { - const parameters = getParameters({}); + const parameters = getParameters({ client: mockApi }); const fields = Object.keys(parameters.shape); expect(fields).toEqual(['pagePath']); diff --git a/typescript/src/shared/tools/documentation/__tests__/getDocumentationSection.test.ts b/typescript/src/shared/tools/documentation/__tests__/getDocumentationSection.test.ts index d11e988..69c3c96 100644 --- a/typescript/src/shared/tools/documentation/__tests__/getDocumentationSection.test.ts +++ b/typescript/src/shared/tools/documentation/__tests__/getDocumentationSection.test.ts @@ -2,11 +2,9 @@ import { execute, getParameters, } from '@/shared/tools/documentation/getDocumentationSection'; -import api from '@/shared/api'; +import { createMockApi } from '@/tests/mockDevelopersApi'; -jest.mock('@/shared/api'); - -const mockApi = api as jest.Mocked; +const mockApi = createMockApi(); describe('execute', () => { beforeEach(() => { @@ -18,8 +16,11 @@ describe('execute', () => { mockApi.getDocumentationSection.mockResolvedValue(mockResult); const result = await execute( - {}, - { serviceId: 'test-service', sectionId: 'test-section' } + { client: mockApi }, + { + serviceId: 'test-service', + sectionId: 'test-section', + } ); expect(mockApi.getDocumentationSection).toHaveBeenCalledWith( @@ -34,8 +35,10 @@ describe('execute', () => { mockApi.getDocumentationSection.mockResolvedValue(mockResult); const result = await execute( - { serviceId: 'context-service' }, - { sectionId: 'test-section' } + { client: mockApi, serviceId: 'context-service' }, + { + sectionId: 'test-section', + } ); expect(mockApi.getDocumentationSection).toHaveBeenCalledWith( @@ -48,7 +51,7 @@ describe('execute', () => { describe('getParameters', () => { it('should return the correct parameters if no context', () => { - const parameters = getParameters({}); + const parameters = getParameters({ client: mockApi }); const fields = Object.keys(parameters.shape); expect(fields).toEqual(['serviceId', 'sectionId']); @@ -56,7 +59,10 @@ describe('getParameters', () => { }); it('should return the correct parameters if serviceId is specified in context', () => { - const parameters = getParameters({ serviceId: 'test-service' }); + const parameters = getParameters({ + client: mockApi, + serviceId: 'test-service', + }); const fields = Object.keys(parameters.shape); expect(fields).toEqual(['sectionId']); diff --git a/typescript/src/shared/tools/documentation/__tests__/getOAuth10aGuide.test.ts b/typescript/src/shared/tools/documentation/__tests__/getOAuth10aGuide.test.ts index dc86616..fb48f68 100644 --- a/typescript/src/shared/tools/documentation/__tests__/getOAuth10aGuide.test.ts +++ b/typescript/src/shared/tools/documentation/__tests__/getOAuth10aGuide.test.ts @@ -2,14 +2,13 @@ import { execute, getParameters, } from '@/shared/tools/documentation/getOAuth10aGuide'; -import api from '@/shared/api'; +import { createMockApi } from '@/tests/mockDevelopersApi'; import fetch from 'node-fetch'; -jest.mock('@/shared/api'); jest.mock('node-fetch'); -const mockApi = api as jest.Mocked; const mockFetch = fetch as jest.MockedFunction; +const mockApi = createMockApi(); describe('execute', () => { beforeEach(() => { @@ -22,7 +21,10 @@ describe('execute', () => { const mockResult = 'mock OAuth 1.0a guide content'; mockApi.getDocumentationPage.mockResolvedValue(mockResult); - const result = await execute({}, { language: language as any }); + const result = await execute( + { client: mockApi }, + { language: language as any } + ); expect(mockApi.getDocumentationPage).toHaveBeenCalledWith( '/platform/documentation/authentication/using-oauth-1a-to-access-mastercard-apis/index.md' @@ -48,7 +50,10 @@ describe('execute', () => { text: jest.fn().mockResolvedValue(mockGithubContent), } as any); - const result = await execute({}, { language: language as any }); + const result = await execute( + { client: mockApi }, + { language: language as any } + ); expect(mockFetch).toHaveBeenCalledWith( `https://raw.githubusercontent.com/Mastercard/${expectedRepo}/refs/heads/main/README.md` @@ -64,7 +69,7 @@ describe('execute', () => { } as any); mockApi.getDocumentationPage.mockResolvedValue(mockApiResult); - const result = await execute({}, { language: 'java' }); + const result = await execute({ client: mockApi }, { language: 'java' }); expect(mockFetch).toHaveBeenCalledWith( 'https://raw.githubusercontent.com/Mastercard/oauth1-signer-java/refs/heads/main/README.md' @@ -78,7 +83,7 @@ describe('execute', () => { describe('getParameters', () => { it('should return the correct parameters if no context', () => { - const parameters = getParameters({}); + const parameters = getParameters({ client: mockApi }); const fields = Object.keys(parameters.shape); expect(fields).toEqual(['language']); diff --git a/typescript/src/shared/tools/documentation/__tests__/getOAuth20Guide.test.ts b/typescript/src/shared/tools/documentation/__tests__/getOAuth20Guide.test.ts new file mode 100644 index 0000000..55f211c --- /dev/null +++ b/typescript/src/shared/tools/documentation/__tests__/getOAuth20Guide.test.ts @@ -0,0 +1,96 @@ +import { + execute, + getParameters, +} from '@/shared/tools/documentation/getOAuth20Guide'; +import { createMockApi } from '@/tests/mockDevelopersApi'; +import fetch from 'node-fetch'; + +jest.mock('node-fetch'); + +const mockFetch = fetch as jest.MockedFunction; +const mockApi = createMockApi(); + +describe('execute', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it.each([[null], [''], ['others'], ['invalid']])( + 'should get generic OAuth 2.0 integration guide and return it when no or invalid language is specified', + async (language) => { + const mockResult = 'mock OAuth 2.0 guide content'; + mockApi.getDocumentationPage.mockResolvedValue(mockResult); + + const result = await execute( + { client: mockApi }, + { language: language as any } + ); + + expect(mockApi.getDocumentationPage).toHaveBeenCalledWith( + '/platform/documentation/authentication/using-oauth-2-to-access-mastercard-apis/index.md' + ); + expect(result).toBe(mockResult); + } + ); + + it.each([ + ['java', 'oauth2-client-java'], + ['kotlin', 'oauth2-client-java'], + ['javascript', 'oauth2-client-js'], + ['typescript', 'oauth2-client-js'], + ])( + 'should get OAuth 2.0 integration guide with %s language from GitHub', + async (language, expectedRepo) => { + const mockGithubContent = `mock OAuth 2.0 guide content with ${language} examples from GitHub`; + mockFetch.mockResolvedValue({ + ok: true, + text: jest.fn().mockResolvedValue(mockGithubContent), + } as any); + + const result = await execute( + { client: mockApi }, + { language: language as any } + ); + + expect(mockFetch).toHaveBeenCalledWith( + `https://raw.githubusercontent.com/Mastercard/${expectedRepo}/refs/heads/main/README.md` + ); + expect(result).toBe(mockGithubContent); + } + ); + + it('should fallback to generic OAuth 2.0 guide when GitHub fetch fails', async () => { + const mockApiResult = 'mock OAuth 2.0 guide content from API'; + mockFetch.mockResolvedValue({ ok: false } as any); + mockApi.getDocumentationPage.mockResolvedValue(mockApiResult); + + const result = await execute({ client: mockApi }, { language: 'java' }); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://raw.githubusercontent.com/Mastercard/oauth2-client-java/refs/heads/main/README.md' + ); + expect(mockApi.getDocumentationPage).toHaveBeenCalledWith( + '/platform/documentation/authentication/using-oauth-2-to-access-mastercard-apis/index.md' + ); + expect(result).toBe(mockApiResult); + }); +}); + +describe('getParameters', () => { + it('should return the correct parameters', () => { + const parameters = getParameters({ client: mockApi }); + + const fields = Object.keys(parameters.shape); + expect(fields).toEqual(['language']); + expect(fields.length).toBe(1); + expect(parameters.shape.language).toBeDefined(); + expect(parameters.shape.language.isOptional()).toBe(true); + }); + + it('should not accept languages unsupported by OAuth 2.0', () => { + const schema = getParameters({ client: mockApi }); + expect(schema.safeParse({ language: 'c#' }).success).toBe(false); + expect(schema.safeParse({ language: 'python' }).success).toBe(false); + expect(schema.safeParse({ language: 'golang' }).success).toBe(false); + }); +}); diff --git a/typescript/src/shared/tools/documentation/__tests__/getOpenFinanceGuide.test.ts b/typescript/src/shared/tools/documentation/__tests__/getOpenFinanceGuide.test.ts index 489b655..352a3a7 100644 --- a/typescript/src/shared/tools/documentation/__tests__/getOpenFinanceGuide.test.ts +++ b/typescript/src/shared/tools/documentation/__tests__/getOpenFinanceGuide.test.ts @@ -2,11 +2,9 @@ import { execute, getParameters, } from '@/shared/tools/documentation/getOpenFinanceGuide'; -import api from '@/shared/api'; +import { createMockApi } from '@/tests/mockDevelopersApi'; -jest.mock('@/shared/api'); - -const mockApi = api as jest.Mocked; +const mockApi = createMockApi(); describe('execute', () => { beforeEach(() => { @@ -17,7 +15,7 @@ describe('execute', () => { const mockResult = 'mock Open Finance guide content'; mockApi.getDocumentationPage.mockResolvedValue(mockResult); - const result = await execute({}, {}); + const result = await execute({ client: mockApi }, {}); expect(mockApi.getDocumentationPage).toHaveBeenCalledWith( '/open-finance-us/documentation/quick-start-guide/index.md' @@ -28,7 +26,7 @@ describe('execute', () => { describe('getParameters', () => { it('should return the correct parameters if no context', () => { - const parameters = getParameters({}); + const parameters = getParameters({ client: mockApi }); const fields = Object.keys(parameters.shape); expect(fields).toEqual([]); diff --git a/typescript/src/shared/tools/documentation/getDocumentation.ts b/typescript/src/shared/tools/documentation/getDocumentation.ts index cccda58..e5be189 100644 --- a/typescript/src/shared/tools/documentation/getDocumentation.ts +++ b/typescript/src/shared/tools/documentation/getDocumentation.ts @@ -1,6 +1,5 @@ import { z } from 'zod'; import { Tool, ToolContext } from '@/shared/types'; -import api from '@/shared/api'; const getDescription = (context: ToolContext): string => { const baseDescription = `Provides an overview of all available documentation for a specific Mastercard service @@ -37,8 +36,9 @@ export const execute = async ( context: ToolContext, params: z.infer> ): Promise => { - const serviceId = context.serviceId || params.serviceId; - return await api.getDocumentation(serviceId); + return await context.client.getDocumentation( + context.serviceId || params.serviceId + ); }; export const getDocumentation = (context: ToolContext): Tool => ({ diff --git a/typescript/src/shared/tools/documentation/getDocumentationPage.ts b/typescript/src/shared/tools/documentation/getDocumentationPage.ts index cb3b500..01a75f9 100644 --- a/typescript/src/shared/tools/documentation/getDocumentationPage.ts +++ b/typescript/src/shared/tools/documentation/getDocumentationPage.ts @@ -1,19 +1,19 @@ import { z } from 'zod'; import { Tool, ToolContext } from '@/shared/types'; -import api from '@/shared/api'; -const getDescription = (_context: ToolContext): string => { +const getDescription = (): string => { return `Retrieves the complete content of a specific documentation page. Takes one argument: - pagePath (str): The full path to the documentation page (e.g., '/send/documentation/use-cases/index.md')`; }; -export const getParameters = (_context: ToolContext): z.ZodObject => { +export const getParameters = (): z.ZodObject => { return z.object({ pagePath: z .string() .min(1) + .startsWith('/') .describe( "The full path to the documentation page (e.g., '/send/documentation/use-cases/index.md')" ), @@ -21,17 +21,17 @@ export const getParameters = (_context: ToolContext): z.ZodObject => { }; export const execute = async ( - _context: ToolContext, + context: ToolContext, params: z.infer> ): Promise => { - return await api.getDocumentationPage(params.pagePath); + return await context.client.getDocumentationPage(params.pagePath); }; export const getDocumentationPage = (context: ToolContext): Tool => ({ name: 'get-documentation-page', title: 'Get Documentation Page', - description: getDescription(context), - parameters: getParameters(context), + description: getDescription(), + parameters: getParameters(), annotations: { readOnlyHint: true, destructiveHint: false, diff --git a/typescript/src/shared/tools/documentation/getDocumentationSection.ts b/typescript/src/shared/tools/documentation/getDocumentationSection.ts index ee1f518..8b644f0 100644 --- a/typescript/src/shared/tools/documentation/getDocumentationSection.ts +++ b/typescript/src/shared/tools/documentation/getDocumentationSection.ts @@ -1,10 +1,9 @@ import { z } from 'zod'; import { Tool, ToolContext } from '@/shared/types'; -import api from '@/shared/api'; const getDescription = (context: ToolContext): string => { const baseDescription = ` -Retrieves the complete content for a specific documentation section. +Retrieves the complete content for a specific documentation section. IMPORTANT: A section is not a single page, but rather a collection of pages that are grouped together. `; @@ -54,8 +53,10 @@ export const execute = async ( context: ToolContext, params: z.infer> ): Promise => { - const serviceId = context.serviceId || params.serviceId; - return await api.getDocumentationSection(serviceId, params.sectionId); + return await context.client.getDocumentationSection( + context.serviceId || params.serviceId, + params.sectionId + ); }; export const getDocumentationSection = (context: ToolContext): Tool => ({ diff --git a/typescript/src/shared/tools/documentation/getOAuth10aGuide.ts b/typescript/src/shared/tools/documentation/getOAuth10aGuide.ts index 2f625d1..c1482c9 100644 --- a/typescript/src/shared/tools/documentation/getOAuth10aGuide.ts +++ b/typescript/src/shared/tools/documentation/getOAuth10aGuide.ts @@ -1,15 +1,14 @@ import { z } from 'zod'; import fetch from 'node-fetch'; import { Tool, ToolContext } from '@/shared/types'; -import api from '@/shared/api'; -const getDescription = (_context: ToolContext): string => { +const getDescription = (): string => { return `Retrieves the comprehensive OAuth 1.0a integration guide including step-by-step instructions, code examples, and best practices for Mastercard APIs. Optionally specify a programming language to get language-specific examples and guidance.`; }; -export const getParameters = (_context: ToolContext): z.ZodObject => { +export const getParameters = (): z.ZodObject => { return z.object({ language: z .enum([ @@ -30,7 +29,7 @@ export const getParameters = (_context: ToolContext): z.ZodObject => { }; export const execute = async ( - _context: ToolContext, + context: ToolContext, params: z.infer> ): Promise => { const basePath = @@ -69,14 +68,14 @@ export const execute = async ( } // Fallback to fetching the general OAuth 1.0a guide - return await api.getDocumentationPage(basePath); + return await context.client.getDocumentationPage(basePath); }; export const getOAuth10aGuide = (context: ToolContext): Tool => ({ name: 'get-oauth10a-integration-guide', title: 'Get OAuth 1.0a Integration Guide', - description: getDescription(context), - parameters: getParameters(context), + description: getDescription(), + parameters: getParameters(), annotations: { readOnlyHint: true, destructiveHint: false, diff --git a/typescript/src/shared/tools/documentation/getOAuth20Guide.ts b/typescript/src/shared/tools/documentation/getOAuth20Guide.ts new file mode 100644 index 0000000..c5f716e --- /dev/null +++ b/typescript/src/shared/tools/documentation/getOAuth20Guide.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; +import fetch from 'node-fetch'; +import { Tool, ToolContext } from '@/shared/types'; + +const getDescription = (): string => { + return `Retrieves the comprehensive OAuth 2.0 integration guide including step-by-step instructions, +code examples, and best practices for Mastercard APIs. Optionally specify a programming language +to get language-specific examples and guidance.`; +}; + +export const getParameters = (): z.ZodObject => { + return z.object({ + language: z + .enum(['java', 'kotlin', 'javascript', 'typescript', 'others']) + .optional() + .describe( + 'Programming language for language-specific examples and guidance' + ), + }); +}; + +export const execute = async ( + context: ToolContext, + params: z.infer> +): Promise => { + const basePath = + '/platform/documentation/authentication/using-oauth-2-to-access-mastercard-apis/index.md'; + + if (params.language) { + let repositoryName: string | undefined; + switch (params.language) { + case 'java': + case 'kotlin': + repositoryName = 'oauth2-client-java'; + break; + case 'javascript': + case 'typescript': + repositoryName = 'oauth2-client-js'; + break; + } + + if (repositoryName !== undefined) { + const githubUrl = `https://raw.githubusercontent.com/Mastercard/${repositoryName}/refs/heads/main/README.md`; + const response = await fetch(githubUrl); + + if (response.ok) { + return await response.text(); + } + } + } + + // Fallback to fetching the general OAuth 2.0 guide + return await context.client.getDocumentationPage(basePath); +}; + +export const getOAuth20Guide = (context: ToolContext): Tool => ({ + name: 'get-oauth20-integration-guide', + title: 'Get OAuth 2.0 Integration Guide', + description: getDescription(), + parameters: getParameters(), + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + execute: (params) => execute(context, params), +}); diff --git a/typescript/src/shared/tools/documentation/getOpenFinanceGuide.ts b/typescript/src/shared/tools/documentation/getOpenFinanceGuide.ts index 762d9e0..9f77b5a 100644 --- a/typescript/src/shared/tools/documentation/getOpenFinanceGuide.ts +++ b/typescript/src/shared/tools/documentation/getOpenFinanceGuide.ts @@ -1,21 +1,20 @@ import { z } from 'zod'; import { Tool, ToolContext } from '@/shared/types'; -import api from '@/shared/api'; -const getDescription = (_context: ToolContext): string => { - return `Retrieves the comprehensive Open Finance (previously known as Open Banking) integration +const getDescription = (): string => { + return `Retrieves the comprehensive Open Finance (previously known as Open Banking) integration guide including setup instructions, API usage examples, and implementation best practices.`; }; -export const getParameters = (_context: ToolContext): z.ZodObject => { +export const getParameters = (): z.ZodObject => { return z.object({}); }; export const execute = async ( - _context: ToolContext, + context: ToolContext, _params: z.infer> ): Promise => { - return await api.getDocumentationPage( + return await context.client.getDocumentationPage( '/open-finance-us/documentation/quick-start-guide/index.md' ); }; @@ -23,8 +22,8 @@ export const execute = async ( export const getOpenFinanceGuide = (context: ToolContext): Tool => ({ name: 'get-openfinance-integration-guide', title: 'Get Open Finance Integration Guide', - description: getDescription(context), - parameters: getParameters(context), + description: getDescription(), + parameters: getParameters(), annotations: { readOnlyHint: true, destructiveHint: false, diff --git a/typescript/src/shared/tools/index.ts b/typescript/src/shared/tools/index.ts index 3b1c1a9..d1b7ffd 100644 --- a/typescript/src/shared/tools/index.ts +++ b/typescript/src/shared/tools/index.ts @@ -5,6 +5,7 @@ import { getDocumentation } from '@/shared/tools/documentation/getDocumentation' import { getDocumentationSection } from '@/shared/tools/documentation/getDocumentationSection'; import { getDocumentationPage } from '@/shared/tools/documentation/getDocumentationPage'; import { getOAuth10aGuide } from '@/shared/tools/documentation/getOAuth10aGuide'; +import { getOAuth20Guide } from '@/shared/tools/documentation/getOAuth20Guide'; import { getOpenFinanceGuide } from '@/shared/tools/documentation/getOpenFinanceGuide'; // Services tools @@ -23,6 +24,7 @@ export const tools = (context: ToolContext): Tool[] => [ getDocumentationSection(context), getDocumentationPage(context), getOAuth10aGuide(context), + getOAuth20Guide(context), getOpenFinanceGuide(context), // API Operations diff --git a/typescript/src/shared/tools/operations/__tests__/getApiOperationDetails.test.ts b/typescript/src/shared/tools/operations/__tests__/getApiOperationDetails.test.ts index f5c0483..0a066bc 100644 --- a/typescript/src/shared/tools/operations/__tests__/getApiOperationDetails.test.ts +++ b/typescript/src/shared/tools/operations/__tests__/getApiOperationDetails.test.ts @@ -2,11 +2,9 @@ import { execute, getParameters, } from '@/shared/tools/operations/getApiOperationDetails'; -import api from '@/shared/api'; +import { createMockApi } from '@/tests/mockDevelopersApi'; -jest.mock('@/shared/api'); - -const mockApi = api as jest.Mocked; +const mockApi = createMockApi(); describe('execute', () => { beforeEach(() => { @@ -18,7 +16,7 @@ describe('execute', () => { mockApi.getApiOperationDetails.mockResolvedValue(mockResult); const result = await execute( - {}, + { client: mockApi }, { apiSpecificationPath: '/test/path.yaml', method: 'GET', @@ -39,7 +37,7 @@ describe('execute', () => { mockApi.getApiOperationDetails.mockResolvedValue(mockResult); const result = await execute( - { apiSpecificationPath: '/context/path.yaml' }, + { client: mockApi, apiSpecificationPath: '/context/path.yaml' }, { method: 'POST', path: '/context/endpoint', @@ -57,7 +55,7 @@ describe('execute', () => { describe('getParameters', () => { it('should return the correct parameters if no context', () => { - const parameters = getParameters({}); + const parameters = getParameters({ client: mockApi }); const fields = Object.keys(parameters.shape); expect(fields).toEqual(['apiSpecificationPath', 'method', 'path']); @@ -66,6 +64,7 @@ describe('getParameters', () => { it('should return the correct parameters if apiSpecificationPath is specified in context', () => { const parameters = getParameters({ + client: mockApi, apiSpecificationPath: '/test/path.yaml', }); diff --git a/typescript/src/shared/tools/operations/__tests__/getApiOperationList.test.ts b/typescript/src/shared/tools/operations/__tests__/getApiOperationList.test.ts index c9582c3..832d8d8 100644 --- a/typescript/src/shared/tools/operations/__tests__/getApiOperationList.test.ts +++ b/typescript/src/shared/tools/operations/__tests__/getApiOperationList.test.ts @@ -2,11 +2,9 @@ import { execute, getParameters, } from '@/shared/tools/operations/getApiOperationList'; -import api from '@/shared/api'; +import { createMockApi } from '@/tests/mockDevelopersApi'; -jest.mock('@/shared/api'); - -const mockApi = api as jest.Mocked; +const mockApi = createMockApi(); describe('execute', () => { beforeEach(() => { @@ -18,8 +16,10 @@ describe('execute', () => { mockApi.getApiOperations.mockResolvedValue(mockResult); const result = await execute( - {}, - { apiSpecificationPath: '/test/path.yaml' } + { client: mockApi }, + { + apiSpecificationPath: '/test/path.yaml', + } ); expect(mockApi.getApiOperations).toHaveBeenCalledWith('/test/path.yaml'); @@ -31,7 +31,7 @@ describe('execute', () => { mockApi.getApiOperations.mockResolvedValue(mockResult); const result = await execute( - { apiSpecificationPath: '/context/path.yaml' }, + { client: mockApi, apiSpecificationPath: '/context/path.yaml' }, {} ); @@ -42,7 +42,7 @@ describe('execute', () => { describe('getParameters', () => { it('should return the correct parameters if no context', () => { - const parameters = getParameters({}); + const parameters = getParameters({ client: mockApi }); const fields = Object.keys(parameters.shape); expect(fields).toEqual(['apiSpecificationPath']); @@ -51,6 +51,7 @@ describe('getParameters', () => { it('should return the correct parameters if apiSpecificationPath is specified in context', () => { const parameters = getParameters({ + client: mockApi, apiSpecificationPath: '/test/path.yaml', }); diff --git a/typescript/src/shared/tools/operations/getApiOperationDetails.ts b/typescript/src/shared/tools/operations/getApiOperationDetails.ts index e02d844..a3a8aff 100644 --- a/typescript/src/shared/tools/operations/getApiOperationDetails.ts +++ b/typescript/src/shared/tools/operations/getApiOperationDetails.ts @@ -1,6 +1,5 @@ import { z } from 'zod'; import { Tool, ToolContext } from '@/shared/types'; -import api from '@/shared/api'; const getDescription = (context: ToolContext): string => { const baseDescription = `Provides detailed information about a specific API operation including parameter definitions, @@ -27,15 +26,19 @@ or /open-finance-us/swagger/openbanking-us.yaml) - path (str): The API endpoint path from the specification (e.g., /payments, /accounts/{id})`; }; +const httpMethod = z + .string() + .transform((value) => value.toUpperCase()) + .pipe(z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])); + export const getParameters = (context: ToolContext): z.ZodObject => { const baseParams = { - method: z - .string() - .describe( - 'The HTTP method of the operation (e.g., GET, POST, PUT, DELETE)' - ), + method: httpMethod.describe( + 'The HTTP method of the operation (e.g., GET, POST, PUT, DELETE)' + ), path: z .string() + .startsWith('/') .describe( 'The API endpoint path from the specification (e.g., /payments, /accounts/{id})' ), @@ -48,6 +51,7 @@ export const getParameters = (context: ToolContext): z.ZodObject => { return z.object({ apiSpecificationPath: z .string() + .startsWith('/') .describe( 'The path to the API specification (e.g., /open-finance-us/swagger/openbanking-us.yaml)' ), @@ -59,19 +63,11 @@ export const execute = async ( context: ToolContext, params: z.infer> ): Promise => { - if (context.apiSpecificationPath) { - return await api.getApiOperationDetails( - context.apiSpecificationPath, - params.method, - params.path - ); - } else { - return await api.getApiOperationDetails( - params.apiSpecificationPath, - params.method, - params.path - ); - } + return await context.client.getApiOperationDetails( + context.apiSpecificationPath || params.apiSpecificationPath, + params.method, + params.path + ); }; export const getApiOperationDetails = (context: ToolContext): Tool => ({ diff --git a/typescript/src/shared/tools/operations/getApiOperationList.ts b/typescript/src/shared/tools/operations/getApiOperationList.ts index 2d2697d..2a9fc15 100644 --- a/typescript/src/shared/tools/operations/getApiOperationList.ts +++ b/typescript/src/shared/tools/operations/getApiOperationList.ts @@ -1,9 +1,8 @@ import { z } from 'zod'; import { Tool, ToolContext } from '@/shared/types'; -import api from '@/shared/api'; const getDescription = (context: ToolContext): string => { - const baseDescription = `Provides a summary of all API operations for a specific Mastercard API + const baseDescription = `Provides a summary of all API operations for a specific Mastercard API specification including HTTP methods, request paths, titles, and descriptions.`; if (context.apiSpecificationPath) { @@ -26,6 +25,7 @@ export const getParameters = (context: ToolContext): z.ZodObject => { return z.object({ apiSpecificationPath: z .string() + .startsWith('/') .describe( 'The path to the API specification file (e.g., /open-finance-us/swagger/openbanking-us.yaml)' ), @@ -36,11 +36,9 @@ export const execute = async ( context: ToolContext, params: z.infer> ): Promise => { - if (context.apiSpecificationPath) { - return await api.getApiOperations(context.apiSpecificationPath); - } else { - return await api.getApiOperations(params.apiSpecificationPath); - } + return await context.client.getApiOperations( + context.apiSpecificationPath || params.apiSpecificationPath + ); }; export const getApiOperationList = (context: ToolContext): Tool => ({ diff --git a/typescript/src/shared/tools/services/__tests__/getServicesList.test.ts b/typescript/src/shared/tools/services/__tests__/getServicesList.test.ts index 13921d6..a0c0448 100644 --- a/typescript/src/shared/tools/services/__tests__/getServicesList.test.ts +++ b/typescript/src/shared/tools/services/__tests__/getServicesList.test.ts @@ -2,11 +2,9 @@ import { execute, getParameters, } from '@/shared/tools/services/getServicesList'; -import api from '@/shared/api'; +import { createMockApi } from '@/tests/mockDevelopersApi'; -jest.mock('@/shared/api'); - -const mockApi = api as jest.Mocked; +const mockApi = createMockApi(); describe('execute', () => { beforeEach(() => { @@ -17,7 +15,7 @@ describe('execute', () => { const mockResult = 'mock services list'; mockApi.listServices.mockResolvedValue(mockResult); - const result = await execute({}, {}); + const result = await execute({ client: mockApi }, {}); expect(mockApi.listServices).toHaveBeenCalledTimes(1); expect(result).toBe(mockResult); @@ -26,7 +24,7 @@ describe('execute', () => { describe('getParameters', () => { it('should return the correct parameters if no context', () => { - const parameters = getParameters({}); + const parameters = getParameters({ client: mockApi }); const fields = Object.keys(parameters.shape); expect(fields).toEqual([]); diff --git a/typescript/src/shared/tools/services/getServicesList.ts b/typescript/src/shared/tools/services/getServicesList.ts index 826c4a6..0a83835 100644 --- a/typescript/src/shared/tools/services/getServicesList.ts +++ b/typescript/src/shared/tools/services/getServicesList.ts @@ -1,30 +1,29 @@ import { z } from 'zod'; import { Tool, ToolContext } from '@/shared/types'; -import api from '@/shared/api'; -const getDescription = (_context: ToolContext): string => { - return `Lists all available Mastercard Developers Products and Services with their basic information +const getDescription = (): string => { + return `Lists all available Mastercard Developers Products and Services with their basic information including title, description, and service id. IMPORTANT: The response contains both 'Products' (business offerings) and 'Services' (technical APIs with serviceIds). Use "serviceId" for each service for any tools that require serviceId as the parameter. `; }; -export const getParameters = (_context: ToolContext): z.ZodObject => { +export const getParameters = (): z.ZodObject => { return z.object({}); }; export const execute = async ( - _context: ToolContext, + context: ToolContext, _params: z.infer> ): Promise => { - return await api.listServices(); + return await context.client.listServices(); }; export const getServicesList = (context: ToolContext): Tool => ({ name: 'get-services-list', title: 'Get Services List', - description: getDescription(context), - parameters: getParameters(context), + description: getDescription(), + parameters: getParameters(), annotations: { readOnlyHint: true, destructiveHint: false, diff --git a/typescript/src/shared/types.ts b/typescript/src/shared/types.ts index ffd65bc..9eca094 100644 --- a/typescript/src/shared/types.ts +++ b/typescript/src/shared/types.ts @@ -10,7 +10,24 @@ export interface Tool { execute: (params: any) => Promise; } +export interface DevelopersApi { + listServices(): Promise; + getDocumentation(serviceId: string): Promise; + getDocumentationSection( + serviceId: string, + sectionId: string + ): Promise; + getDocumentationPage(pagePath: string): Promise; + getApiOperations(apiSpecificationPath: string): Promise; + getApiOperationDetails( + apiSpecificationPath: string, + method: string, + path: string + ): Promise; +} + export interface ToolContext { + client: DevelopersApi; serviceId?: string; apiSpecificationPath?: string; } diff --git a/typescript/src/tests/mockDevelopersApi.ts b/typescript/src/tests/mockDevelopersApi.ts new file mode 100644 index 0000000..e003796 --- /dev/null +++ b/typescript/src/tests/mockDevelopersApi.ts @@ -0,0 +1,10 @@ +import type { DevelopersApi } from '@/shared/types'; + +export const createMockApi = (): jest.Mocked => ({ + listServices: jest.fn(), + getDocumentation: jest.fn(), + getDocumentationSection: jest.fn(), + getDocumentationPage: jest.fn(), + getApiOperations: jest.fn(), + getApiOperationDetails: jest.fn(), +});