From c9d5ce94610be3a7e38120193bd34c35f10d942c Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Thu, 23 Apr 2026 13:27:39 +0200 Subject: [PATCH 1/7] feat: add functionality to list app modules with credentials --- src/endpoints/credential-requests.mcp.ts | 36 ++++++++++++++ src/endpoints/credential-requests.ts | 47 +++++++++++++++++++ src/index.ts | 1 + test/credential-requests.integration.test.ts | 14 ++++++ test/credential-requests.spec.ts | 34 ++++++++++++++ .../list-app-modules-with-credentials.json | 37 +++++++++++++++ 6 files changed, 169 insertions(+) create mode 100644 test/mocks/credential-requests/list-app-modules-with-credentials.json diff --git a/src/endpoints/credential-requests.mcp.ts b/src/endpoints/credential-requests.mcp.ts index 389977d..2a9712c 100644 --- a/src/endpoints/credential-requests.mcp.ts +++ b/src/endpoints/credential-requests.mcp.ts @@ -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'], + 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); + }, + }, ]; diff --git a/src/endpoints/credential-requests.ts b/src/endpoints/credential-requests.ts index 33c861a..ce33715 100644 --- a/src/endpoints/credential-requests.ts +++ b/src/endpoints/credential-requests.ts @@ -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:` or `keychain:`. + * 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 */ @@ -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 { + const response = await this.#fetch<{ appModules: AppModuleWithCredentials[] }>( + `/credential-requests/apps/${encodeURIComponent(appName)}/${appVersion}/modules-with-credentials`, + ); + return response.appModules; + } } /** diff --git a/src/index.ts b/src/index.ts index adad5d0..c540644 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ export type { GetCredentialRequestOptions, Credential, ListCredentialRequestsOptions, + AppModuleWithCredentials, } from './endpoints/credential-requests.js'; export type { DataStore, diff --git a/test/credential-requests.integration.test.ts b/test/credential-requests.integration.test.ts index 56e4417..e1a4a6d 100644 --- a/test/credential-requests.integration.test.ts +++ b/test/credential-requests.integration.test.ts @@ -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); + expect(typeof module.hook).toBe('boolean'); + }); }); diff --git a/test/credential-requests.spec.ts b/test/credential-requests.spec.ts index afa508a..e998b8c 100644 --- a/test/credential-requests.spec.ts +++ b/test/credential-requests.spec.ts @@ -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'; @@ -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); + }); }); diff --git a/test/mocks/credential-requests/list-app-modules-with-credentials.json b/test/mocks/credential-requests/list-app-modules-with-credentials.json new file mode 100644 index 0000000..02b4562 --- /dev/null +++ b/test/mocks/credential-requests/list-app-modules-with-credentials.json @@ -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 + } + ] +} From 227e76d8dcc7642bcc8151678acba724fe57a370 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Thu, 23 Apr 2026 13:53:41 +0200 Subject: [PATCH 2/7] feat: update appVersion type to allow 'latest' string option --- src/endpoints/credential-requests.mcp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/credential-requests.mcp.ts b/src/endpoints/credential-requests.mcp.ts index 2a9712c..d5841eb 100644 --- a/src/endpoints/credential-requests.mcp.ts +++ b/src/endpoints/credential-requests.mcp.ts @@ -465,7 +465,7 @@ export const tools = [ 'App name (e.g. `slack`). For custom/SDK apps, prefix with `app#` (e.g. `app#my-custom-app`).', }, appVersion: { - type: ['number', 'string'], + oneOf: [{ type: 'number' }, { type: 'string', const: 'latest' }], description: 'App major version number (e.g. `4`), or the literal string `"latest"`.', }, }, From 829245fe3e2d9b123dfa8d19fecec7975cd40989 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Fri, 24 Apr 2026 09:50:14 +0200 Subject: [PATCH 3/7] feat: make OAuth scopes optional for non-OAuth credential types --- src/endpoints/credential-requests.ts | 7 +++++-- .../list-app-modules-with-credentials.json | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/endpoints/credential-requests.ts b/src/endpoints/credential-requests.ts index ce33715..be4d7ad 100644 --- a/src/endpoints/credential-requests.ts +++ b/src/endpoints/credential-requests.ts @@ -227,8 +227,11 @@ export type AppModuleWithCredentials = { * Multiple types may be comma-separated (e.g. `account:slack2,slack3`). */ type: string; - /** OAuth scopes required by this module */ - scope: string[]; + /** + * OAuth scopes required by this module. + * Omitted by the API for non-OAuth credential types (e.g. keychain-based auth). + */ + scope?: string[]; /** Whether this module is a hook-based trigger */ hook: boolean; }; diff --git a/test/mocks/credential-requests/list-app-modules-with-credentials.json b/test/mocks/credential-requests/list-app-modules-with-credentials.json index 02b4562..ca5ff8b 100644 --- a/test/mocks/credential-requests/list-app-modules-with-credentials.json +++ b/test/mocks/credential-requests/list-app-modules-with-credentials.json @@ -32,6 +32,13 @@ "type": "account:slack2,slack3", "scope": [], "hook": true + }, + { + "id": "MakeRequest:authenticationType:apiKey", + "name": "MakeRequest", + "label": "Make a request - API key", + "type": "keychain:apikeyauth", + "hook": false } ] } From 298c5818797e97e5351eb1a4522fee2cee17c9da Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Fri, 24 Apr 2026 13:28:36 +0200 Subject: [PATCH 4/7] feat: make OAuth scopes optional for non-OAuth credential types --- src/endpoints/credential-requests.tools.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/endpoints/credential-requests.tools.ts b/src/endpoints/credential-requests.tools.ts index da79cd2..f52cd5c 100644 --- a/src/endpoints/credential-requests.tools.ts +++ b/src/endpoints/credential-requests.tools.ts @@ -465,6 +465,7 @@ export const tools = [ 'For custom/SDK apps, prefix the app name with `app#` (e.g. `app#my-custom-app`).', category: 'credential-requests', scope: 'apps:read', + scopeId: 'appName', identifier: 'appName', annotations: { readOnlyHint: true, From 0511ca24ab03af8c952bf42cb0501c00c351667f Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Fri, 24 Apr 2026 17:13:14 +0200 Subject: [PATCH 5/7] feat: make JSONSchema type optional for enhanced flexibility --- src/tools.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/tools.ts b/src/tools.ts index bac7075..47e8da7 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -30,8 +30,12 @@ import { tools as EnumsTools } from './endpoints/enums.tools.js'; * JSON Schema definition for input parameters. */ export type JSONSchema = { - /** The type of the schema (object, string, number, boolean, array, etc.) */ - type: 'object' | 'string' | 'number' | 'boolean' | 'array' | 'null'; + /** + * The type of the schema (object, string, number, boolean, array, etc.). + * Optional when the schema is expressed purely through composition + * (`oneOf`/`anyOf`/`allOf`) or a `const` value. + */ + type?: 'object' | 'string' | 'number' | 'boolean' | 'array' | 'null'; /** Properties definition for object types */ properties?: Record; /** Required property names for object types */ @@ -42,6 +46,14 @@ export type JSONSchema = { description?: string; /** Enum values for restricted choices */ enum?: JSONValue[]; + /** Constant literal value the schema must equal */ + const?: JSONValue; + /** Value must match exactly one of these schemas */ + oneOf?: JSONSchema[]; + /** Value must match at least one of these schemas */ + anyOf?: JSONSchema[]; + /** Value must match all of these schemas */ + allOf?: JSONSchema[]; /** Default value */ default?: JSONValue; /** Minimum value for numbers */ From c050bbbee0b24911bef07bd91917cc7992126513 Mon Sep 17 00:00:00 2001 From: JanKulhavy Date: Fri, 24 Apr 2026 17:35:28 +0200 Subject: [PATCH 6/7] fix: address review feedback on JSONSchema coverage and optional scope - Add minItems/maxItems to JSONSchema (already used by tool defs) and correct minLength/maxLength docs to be string-only per JSON Schema spec. - Align integration test assertion with AppModuleWithCredentials.scope being optional for non-OAuth credential types. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tools.ts | 8 ++++++-- test/credential-requests.integration.test.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/tools.ts b/src/tools.ts index 47e8da7..6f2d51e 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -60,10 +60,14 @@ export type JSONSchema = { minimum?: number; /** Maximum value for numbers */ maximum?: number; - /** Minimum length for strings/arrays */ + /** Minimum length for strings */ minLength?: number; - /** Maximum length for strings/arrays */ + /** Maximum length for strings */ maxLength?: number; + /** Minimum number of items for arrays */ + minItems?: number; + /** Maximum number of items for arrays */ + maxItems?: number; /** Pattern for string validation */ pattern?: string; }; diff --git a/test/credential-requests.integration.test.ts b/test/credential-requests.integration.test.ts index e1a4a6d..73e9eef 100644 --- a/test/credential-requests.integration.test.ts +++ b/test/credential-requests.integration.test.ts @@ -270,7 +270,7 @@ describe('Integration: CredentialRequests', () => { 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); + expect(module.scope === undefined || Array.isArray(module.scope)).toBe(true); expect(typeof module.hook).toBe('boolean'); }); }); From cc99f87817a05a1034a8e4b033e3d69e1d7293a6 Mon Sep 17 00:00:00 2001 From: Patrik Simek Date: Sat, 25 Apr 2026 21:30:35 +0200 Subject: [PATCH 7/7] fix: update test to use specific app version --- test/credential-requests.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/credential-requests.integration.test.ts b/test/credential-requests.integration.test.ts index 73e9eef..cf80892 100644 --- a/test/credential-requests.integration.test.ts +++ b/test/credential-requests.integration.test.ts @@ -261,7 +261,7 @@ describe('Integration: CredentialRequests', () => { }); it('Should list app modules with credentials for a public app', async () => { - const modules = await make.credentialRequests.listAppModulesWithCredentials('google-email', 'latest'); + const modules = await make.credentialRequests.listAppModulesWithCredentials('google-email', 4); expect(Array.isArray(modules)).toBe(true); expect(modules.length).toBeGreaterThan(0);