Skip to content

Commit 8b1be0e

Browse files
authored
Pluggable DevelopersApi and OAuth 2.0 tool (#48)
* Add injectable DevelopersApi support and OAuth 2.0 tool * Simplify tool signatures: move client into `ToolContext` * Update test cases to include client context in execute function
1 parent aefd45e commit 8b1be0e

29 files changed

Lines changed: 377 additions & 151 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The toolkit provides the following tools for agents to use:
2828
* `get-documentation-section-content`: Retrieves the complete content for a specific documentation section.
2929
* `get-documentation-page`: Retrieves the complete content of a specific documentation page.
3030
* `get-oauth10a-integration-guide`: Retrieves the comprehensive OAuth 1.0a integration guide.
31+
* `get-oauth20-integration-guide`: Retrieves the comprehensive OAuth 2.0 integration guide.
3132
* `get-openfinance-integration-guide`: Retrieves the comprehensive Open Finance integration guide.
3233

3334
### API Operations

modelcontextprotocol/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This MCP server acts as a bridge between MCP clients and Mastercard Developers r
1414
- List available Mastercard services
1515
- Query API specifications and their operations
1616
- Access documentation for each service
17-
- Retrieve integration guides for OAuth 1.0a and Open Finance
17+
- Retrieve integration guides for OAuth 1.0a, OAuth 2.0, and Open Finance
1818

1919
## Available Tools
2020

@@ -40,6 +40,8 @@ This MCP server acts as a bridge between MCP clients and Mastercard Developers r
4040

4141
- **`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.
4242

43+
- **`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.
44+
4345
- **`get-openfinance-integration-guide`**: Retrieves the comprehensive Open Finance integration guide including setup instructions, API usage examples, and implementation best practices.
4446

4547
## Configuration Options

typescript/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.1.4",
2+
"version": "0.1.5",
33
"name": "@mastercard/developers-agent-toolkit",
44
"homepage": "https://github.com/mastercard/developers-agent-toolkit",
55
"description": "Agent Toolkit for Mastercard Developers Platform",

typescript/src/modelcontextprotocol/__tests__/index.test.ts

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('MastercardDevelopersAgentToolkit', () => {
3838

3939
it('should list all tools with correct name/description when no config', async () => {
4040
const registeredTools = await listRegisteredTools({});
41-
const expectedTools = tools({});
41+
const expectedTools = tools(buildContext({}));
4242
expect(registeredTools).toHaveLength(expectedTools.length);
4343

4444
expectedTools.forEach((expectedTool, index) => {
@@ -60,10 +60,12 @@ describe('MastercardDevelopersAgentToolkit', () => {
6060
'https://static.developer.mastercard.com/content/service/swagger/path.yaml',
6161
});
6262

63-
const expectedTools = tools({
64-
serviceId: 'service',
65-
apiSpecificationPath: '/service/swagger/path.yaml',
66-
}).filter((tool) => tool.name !== 'get-services-list');
63+
const expectedTools = tools(
64+
buildContext({
65+
apiSpecification:
66+
'https://static.developer.mastercard.com/content/service/swagger/path.yaml',
67+
})
68+
).filter((tool) => tool.name !== 'get-services-list');
6769
expect(registeredTools).toHaveLength(expectedTools.length);
6870

6971
expectedTools.forEach((expectedTool, index) => {
@@ -84,9 +86,11 @@ describe('MastercardDevelopersAgentToolkit', () => {
8486
service: 'https://developer.mastercard.com/test-service/documentation/',
8587
});
8688

87-
const expectedTools = tools({ serviceId: 'test-service' }).filter(
88-
(tool) => tool.name !== 'get-services-list'
89-
);
89+
const expectedTools = tools(
90+
buildContext({
91+
service: 'https://developer.mastercard.com/test-service/documentation/',
92+
})
93+
).filter((tool) => tool.name !== 'get-services-list');
9094
const registeredNames = registeredTools.map((tool) => tool.name);
9195

9296
expect(registeredNames).not.toContain('get-services-list');
@@ -110,49 +114,57 @@ describe('buildContext function', () => {
110114
describe('success cases', () => {
111115
it('should return empty context when no config provided', () => {
112116
const result = buildContext({});
113-
expect(result).toEqual({});
117+
expect(result).toEqual(expect.objectContaining({}));
114118
});
115119

116120
it('should parse service URL and extract serviceId', () => {
117121
const result = buildContext({
118122
service:
119123
'https://developer.mastercard.com/open-finance-us/documentation/',
120124
});
121-
expect(result).toEqual({
122-
serviceId: 'open-finance-us',
123-
});
125+
expect(result).toEqual(
126+
expect.objectContaining({
127+
serviceId: 'open-finance-us',
128+
})
129+
);
124130
});
125131

126132
it('should parse API specification URL and extract serviceId and apiSpecificationPath', () => {
127133
const result = buildContext({
128134
apiSpecification:
129135
'https://static.developer.mastercard.com/content/test-service/swagger/api.yaml',
130136
});
131-
expect(result).toEqual({
132-
serviceId: 'test-service',
133-
apiSpecificationPath: '/test-service/swagger/api.yaml',
134-
});
137+
expect(result).toEqual(
138+
expect.objectContaining({
139+
serviceId: 'test-service',
140+
apiSpecificationPath: '/test-service/swagger/api.yaml',
141+
})
142+
);
135143
});
136144

137145
it('should handle nested API specification paths', () => {
138146
const result = buildContext({
139147
apiSpecification:
140148
'https://static.developer.mastercard.com/content/payment-gateway/swagger/nested/spec.yaml',
141149
});
142-
expect(result).toEqual({
143-
serviceId: 'payment-gateway',
144-
apiSpecificationPath: '/payment-gateway/swagger/nested/spec.yaml',
145-
});
150+
expect(result).toEqual(
151+
expect.objectContaining({
152+
serviceId: 'payment-gateway',
153+
apiSpecificationPath: '/payment-gateway/swagger/nested/spec.yaml',
154+
})
155+
);
146156
});
147157

148158
it('should handle service IDs with hyphens and numbers', () => {
149159
const result = buildContext({
150160
service:
151161
'https://developer.mastercard.com/open-finance-us-v2/documentation/',
152162
});
153-
expect(result).toEqual({
154-
serviceId: 'open-finance-us-v2',
155-
});
163+
expect(result).toEqual(
164+
expect.objectContaining({
165+
serviceId: 'open-finance-us-v2',
166+
})
167+
);
156168
});
157169
});
158170

typescript/src/modelcontextprotocol/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import { defaultDevelopersApi } from '@/shared/api';
23
import { tools } from '@/shared/tools';
34
import { ToolContext } from '@/shared/types';
45
import { version } from '../../package.json';
56

7+
export type { DevelopersApi, Tool, ToolContext } from '@/shared/types';
8+
export { tools } from '@/shared/tools';
9+
610
export interface MastercardDevelopersAgentToolkitConfig {
711
service?: string;
812
apiSpecification?: string;
@@ -61,7 +65,7 @@ export class MastercardDevelopersAgentToolkit extends McpServer {
6165
export function buildContext(
6266
config: MastercardDevelopersAgentToolkitConfig
6367
): ToolContext {
64-
const context: ToolContext = {};
68+
const context: ToolContext = { client: defaultDevelopersApi };
6569
if (config.service != null) {
6670
const serviceId = parseServiceIdFromUrl(config.service);
6771
if (serviceId == null) {

typescript/src/shared/api/__tests__/index.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MastercardAPIClient } from '@/shared/api';
1+
import { defaultDevelopersApi, MastercardAPIClient } from '@/shared/api';
22
import fetch, { RequestInfo, Response } from 'node-fetch';
33

44
const mcd = (path: string) => {
@@ -8,6 +8,12 @@ const mcd = (path: string) => {
88
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
99
jest.mock('node-fetch');
1010

11+
describe('defaultDevelopersApi', () => {
12+
it('uses the default MastercardAPIClient implementation', () => {
13+
expect(defaultDevelopersApi).toBeInstanceOf(MastercardAPIClient);
14+
});
15+
});
16+
1117
describe('MastercardAPIClient', () => {
1218
let client: MastercardAPIClient;
1319

typescript/src/shared/api/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { z } from 'zod';
22
import fetch from 'node-fetch';
33

4+
import type { DevelopersApi } from '@/shared/types';
5+
46
const PathSchema = z
57
.string()
68
.min(1, 'Path must be a non-empty string')
@@ -106,5 +108,4 @@ export class MastercardAPIClient {
106108
}
107109
}
108110

109-
const api = new MastercardAPIClient();
110-
export default api;
111+
export const defaultDevelopersApi: DevelopersApi = new MastercardAPIClient();

typescript/src/shared/tools/documentation/__tests__/getDocumentation.test.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import {
22
execute,
33
getParameters,
44
} from '@/shared/tools/documentation/getDocumentation';
5-
import api from '@/shared/api';
5+
import { createMockApi } from '@/tests/mockDevelopersApi';
66

7-
jest.mock<typeof api>('@/shared/api');
8-
9-
const mockApi = api as jest.Mocked<typeof api>;
7+
const mockApi = createMockApi();
108

119
describe('execute', () => {
1210
beforeEach(() => {
@@ -17,7 +15,10 @@ describe('execute', () => {
1715
const mockResult = 'mock documentation';
1816
mockApi.getDocumentation.mockResolvedValue(mockResult);
1917

20-
const result = await execute({}, { serviceId: 'test-service' });
18+
const result = await execute(
19+
{ client: mockApi },
20+
{ serviceId: 'test-service' }
21+
);
2122

2223
expect(mockApi.getDocumentation).toHaveBeenCalledWith('test-service');
2324
expect(result).toBe(mockResult);
@@ -27,7 +28,10 @@ describe('execute', () => {
2728
const mockResult = 'mock documentation';
2829
mockApi.getDocumentation.mockResolvedValue(mockResult);
2930

30-
const result = await execute({ serviceId: 'context-service' }, {});
31+
const result = await execute(
32+
{ client: mockApi, serviceId: 'context-service' },
33+
{}
34+
);
3135

3236
expect(mockApi.getDocumentation).toHaveBeenCalledWith('context-service');
3337
expect(result).toBe(mockResult);
@@ -36,15 +40,18 @@ describe('execute', () => {
3640

3741
describe('getParameters', () => {
3842
it('should return the correct parameters if no context', () => {
39-
const parameters = getParameters({});
43+
const parameters = getParameters({ client: mockApi });
4044

4145
const fields = Object.keys(parameters.shape);
4246
expect(fields).toEqual(['serviceId']);
4347
expect(fields.length).toBe(1);
4448
});
4549

4650
it('should return the correct parameters if serviceId is specified in context', () => {
47-
const parameters = getParameters({ serviceId: 'test-service' });
51+
const parameters = getParameters({
52+
client: mockApi,
53+
serviceId: 'test-service',
54+
});
4855

4956
const fields = Object.keys(parameters.shape);
5057
expect(fields).toEqual([]);

typescript/src/shared/tools/documentation/__tests__/getDocumentationPage.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import {
22
execute,
33
getParameters,
44
} from '@/shared/tools/documentation/getDocumentationPage';
5-
import api from '@/shared/api';
5+
import { createMockApi } from '@/tests/mockDevelopersApi';
66

7-
jest.mock<typeof api>('@/shared/api');
8-
9-
const mockApi = api as jest.Mocked<typeof api>;
7+
const mockApi = createMockApi();
108

119
describe('execute', () => {
1210
beforeEach(() => {
@@ -17,7 +15,10 @@ describe('execute', () => {
1715
const mockResult = 'mock documentation page content';
1816
mockApi.getDocumentationPage.mockResolvedValue(mockResult);
1917

20-
const result = await execute({}, { pagePath: '/test/page.md' });
18+
const result = await execute(
19+
{ client: mockApi },
20+
{ pagePath: '/test/page.md' }
21+
);
2122

2223
expect(mockApi.getDocumentationPage).toHaveBeenCalledWith('/test/page.md');
2324
expect(result).toBe(mockResult);
@@ -26,7 +27,7 @@ describe('execute', () => {
2627

2728
describe('getParameters', () => {
2829
it('should return the correct parameters if no context', () => {
29-
const parameters = getParameters({});
30+
const parameters = getParameters({ client: mockApi });
3031

3132
const fields = Object.keys(parameters.shape);
3233
expect(fields).toEqual(['pagePath']);

0 commit comments

Comments
 (0)