Skip to content
Merged
36 changes: 36 additions & 0 deletions src/endpoints/credential-requests.mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,4 +443,40 @@ export const tools = [
return await make.credentialRequests.extendConnection(args);
},
},
{
name: 'credential-requests_list-app-modules-with-credentials',
title: 'List app modules with credentials',
description:
'List all modules of a given Make app (and version) that require credentials, along with the required credential type and OAuth scopes. ' +
'Use this to discover which modules exist for an app before constructing a credential request — the returned `id` values are what you pass in `credentials[].appModules` for `credential-requests_create`. ' +
'For custom/SDK apps, prefix the app name with `app#` (e.g. `app#my-custom-app`).',
category: 'credential-requests',
scope: 'apps:read',
identifier: 'appName',
annotations: {
readOnlyHint: true,
},
inputSchema: {
type: 'object',
properties: {
appName: {
type: 'string',
description:
'App name (e.g. `slack`). For custom/SDK apps, prefix with `app#` (e.g. `app#my-custom-app`).',
},
appVersion: {
type: ['number', 'string'],
Comment thread
JanKulhavy marked this conversation as resolved.
Outdated
description: 'App major version number (e.g. `4`), or the literal string `"latest"`.',
},
},
required: ['appName', 'appVersion'],
},
examples: [
{ appName: 'slack', appVersion: 4 },
{ appName: 'slack', appVersion: 'latest' },
],
execute: async (make: Make, args: { appName: string; appVersion: number | 'latest' }) => {
return await make.credentialRequests.listAppModulesWithCredentials(args.appName, args.appVersion);
},
},
];
47 changes: 47 additions & 0 deletions src/endpoints/credential-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,33 @@ export type ExtendConnectionBody = {
scopes: string[];
};

/**
* Module of an app that requires credentials, as returned by
* {@link CredentialRequests.listAppModulesWithCredentials}.
*/
export type AppModuleWithCredentials = {
/**
* Unique identifier for the module credential configuration.
* For modules with a single credential this matches the module `name`;
* for modules with multiple credentials it is suffixed (e.g. `moduleName:paramName`).
*/
id: string;
/** Technical module name */
name: string;
/** Human-readable module label */
label: string;
/**
* Credential type required by the module.
* Typically `account:<app-name>` or `keychain:<app-name>`.
* Multiple types may be comma-separated (e.g. `account:slack2,slack3`).
*/
type: string;
/** OAuth scopes required by this module */
scope: string[];
/** Whether this module is a hook-based trigger */
hook: boolean;
};

/**
* Class providing methods for working with credential requests
*/
Expand Down Expand Up @@ -368,6 +395,26 @@ export class CredentialRequests {
},
);
}

/**
* List all modules of an app that require credentials, along with the required
* credential type and OAuth scopes. Useful for populating
* {@link CredentialSelection.appModules} when creating a credential request.
*
* For SDK/custom apps, prefix `appName` with `app#` — the SDK URL-encodes it.
*
* @param appName - The app name (e.g. `slack`, or `app#my-custom-app` for SDK apps).
* @param appVersion - App major version number, or `'latest'` for the most recent version.
*/
async listAppModulesWithCredentials(
appName: string,
appVersion: number | 'latest',
): Promise<AppModuleWithCredentials[]> {
const response = await this.#fetch<{ appModules: AppModuleWithCredentials[] }>(
`/credential-requests/apps/${encodeURIComponent(appName)}/${appVersion}/modules-with-credentials`,
);
return response.appModules;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type {
GetCredentialRequestOptions,
Credential,
ListCredentialRequestsOptions,
AppModuleWithCredentials,
} from './endpoints/credential-requests.js';
export type {
DataStore,
Expand Down
14 changes: 14 additions & 0 deletions test/credential-requests.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,18 @@ describe('Integration: CredentialRequests', () => {
const requests = await make.credentialRequests.list(MAKE_TEAM);
expect(requests.some(r => r.id === nameOverrideRequestId)).toBe(false);
});

it('Should list app modules with credentials for a public app', async () => {
const modules = await make.credentialRequests.listAppModulesWithCredentials('google-email', 'latest');
expect(Array.isArray(modules)).toBe(true);
expect(modules.length).toBeGreaterThan(0);

const module = modules[0]!;
expect(typeof module.id).toBe('string');
expect(typeof module.name).toBe('string');
expect(typeof module.label).toBe('string');
expect(typeof module.type).toBe('string');
expect(Array.isArray(module.scope)).toBe(true);
Comment thread
JanKulhavy marked this conversation as resolved.
Outdated
expect(typeof module.hook).toBe('boolean');
});
});
34 changes: 34 additions & 0 deletions test/credential-requests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as deleteRemoteMock from './mocks/credential-requests/delete-remote.jso
import * as createActionMock from './mocks/credential-requests/create-action.json';
import * as createByCredentialsMock from './mocks/credential-requests/create-by-credentials.json';
import * as extendConnectionMock from './mocks/credential-requests/extend-connection.json';
import * as listAppModulesWithCredentialsMock from './mocks/credential-requests/list-app-modules-with-credentials.json';

const MAKE_API_KEY = 'api-key';
const MAKE_ZONE = 'make.local';
Expand Down Expand Up @@ -266,4 +267,37 @@ describe('Endpoints: CredentialRequests', () => {
publicUri: extendConnectionMock.publicUri,
});
});

it('Should list app modules with credentials by numeric version', async () => {
mockFetch(
'GET https://make.local/api/v2/credential-requests/apps/slack/4/modules-with-credentials',
listAppModulesWithCredentialsMock,
);

const result = await make.credentialRequests.listAppModulesWithCredentials('slack', 4);

expect(result).toStrictEqual(listAppModulesWithCredentialsMock.appModules);
});

it('Should list app modules with credentials by latest version', async () => {
mockFetch(
'GET https://make.local/api/v2/credential-requests/apps/slack/latest/modules-with-credentials',
listAppModulesWithCredentialsMock,
);

const result = await make.credentialRequests.listAppModulesWithCredentials('slack', 'latest');

expect(result).toStrictEqual(listAppModulesWithCredentialsMock.appModules);
});

it('Should list app modules with credentials for SDK apps (app# prefix URL-encoded)', async () => {
mockFetch(
'GET https://make.local/api/v2/credential-requests/apps/app%23my-custom-app/1/modules-with-credentials',
listAppModulesWithCredentialsMock,
);

const result = await make.credentialRequests.listAppModulesWithCredentials('app#my-custom-app', 1);

expect(result).toStrictEqual(listAppModulesWithCredentialsMock.appModules);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"appModules": [
{
"id": "CreateMessage",
"name": "CreateMessage",
"label": "Send a Message",
"type": "account:slack2,slack3",
"scope": [
"chat:write",
"chat:write.public",
"chat:write.customize",
"channels:read",
"groups:read",
"im:read",
"mpim:read",
"users:read"
],
"hook": false
},
{
"id": "WatchMessages",
"name": "WatchMessages",
"label": "Watch Public Channel Messages",
"type": "account:slack2",
"scope": ["channels:history", "channels:read"],
"hook": false
},
{
"id": "watchInteractivityEvents",
"name": "watchInteractivityEvents",
"label": "Watch Interactive Events",
"type": "account:slack2,slack3",
"scope": [],
"hook": true
}
]
}