Skip to content

Commit d72fc48

Browse files
authored
Merge pull request #96 from mailtrap/add-missing-general-tools
Add missing general tools
2 parents 79b4883 + 019da35 commit d72fc48

40 files changed

Lines changed: 1525 additions & 0 deletions

CLAUDE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,19 @@ Schema files define a JSON Schema–shaped object for MCP; optional Zod schemas
163163
- **create-contact-export**: Export contacts matching AND-combined filters (`name`/`operator`/`value`). Returns an export job; poll for download URL.
164164
- **get-contact-export**: Get the status of a contact export job. `url` is populated when `status: finished`.
165165

166+
167+
#### General / Account-admin
168+
169+
- **list-accounts**: List Mailtrap accounts the API token can access (with each account's `access_levels`).
170+
- **get-billing-usage**: Current billing cycle usage (plans, limits, current counts) for the account.
171+
- **list-account-accesses**: List users/invites/tokens with access to the account, optional filters by domain/inbox/project. Requires admin.
172+
- **remove-account-access**: Revoke permissions (User) or delete the specifier (Invite/ApiToken). Requires admin.
173+
- **get-permission-resources**: Hierarchical list of inboxes/projects/domains/billing/account the token has admin access to.
174+
- **bulk-update-permissions**: Create/update/destroy multiple permissions on an account access in one call.
175+
- **list-api-tokens**: List all API tokens for the account.
176+
- **create-api-token**: Create an API token. Response includes the secret `token` value (returned only on creation — must be stored).
177+
- **get-api-token**: Get an API token by ID (metadata only — secret is not returned).
178+
- **reset-api-token**: Rotate an API token. Response includes the new secret `token` value; previous one is invalidated.
179+
- **delete-api-token**: Permanently delete an API token by ID.
180+
166181
Tools use input schemas (JSON Schema format) for MCP; handlers may validate input with Zod. Response format follows the MCP protocol.

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,105 @@ Get the status of a contact export job. Once `status` is `finished`, the `url` f
827827

828828
- `export_id` (required): ID of the contact export job
829829

830+
### list-accounts
831+
832+
List Mailtrap accounts the current API token can access, with each account's access levels.
833+
834+
**Parameters:**
835+
836+
- No parameters required
837+
838+
### get-billing-usage
839+
840+
Get the current billing cycle usage for the account: sending and testing plans, limits, and current counts.
841+
842+
**Parameters:**
843+
844+
- No parameters required
845+
846+
### list-account-accesses
847+
848+
List account accesses (users, invites, API tokens) for the account. Optional filters narrow the result to specific resources. Requires account admin/owner permissions.
849+
850+
**Parameters:**
851+
852+
- `domain_uuids` (optional): Filter by sending domain UUIDs (array of strings)
853+
- `inbox_ids` (optional): Filter by sandbox inbox IDs (array of strings)
854+
- `project_ids` (optional): Filter by sandbox project IDs (array of strings)
855+
856+
### remove-account-access
857+
858+
Remove an account access by ID. For `User` specifiers this revokes their permissions; for `Invite` or `ApiToken` specifiers it removes the specifier entirely. Requires admin/owner.
859+
860+
**Parameters:**
861+
862+
- `account_access_id` (required): ID of the access record to remove
863+
864+
### get-permission-resources
865+
866+
Get all resources (inboxes, projects, domains, billing, account) to which the API token has admin access, nested by hierarchy.
867+
868+
**Parameters:**
869+
870+
- No parameters required
871+
872+
### bulk-update-permissions
873+
874+
Bulk create, update, or destroy permissions for a single account access. Existing `(resource_type, resource_id)` pairs are updated; new ones are created. Set `destroy: true` on an entry to remove it.
875+
876+
**Parameters:**
877+
878+
- `account_access_id` (required): Target account access ID
879+
- `permissions` (required): Array of permission entries. Each has:
880+
- `resource_id` (required): Resource ID (number or string)
881+
- `resource_type` (required): One of `account`, `project`, `inbox`, `domain`, `billing`
882+
- `access_level` (optional): `admin`/`100` or `viewer`/`10`
883+
- `destroy` (optional, boolean): When true, removes this permission instead of creating/updating it
884+
885+
### list-api-tokens
886+
887+
List all API tokens for the account.
888+
889+
**Parameters:**
890+
891+
- No parameters required
892+
893+
### create-api-token
894+
895+
Create a new API token. The response includes the secret `token` value — this is the **only time** the full token is returned, so store it immediately. If you lose it, recreate the token.
896+
897+
**Parameters:**
898+
899+
- `name` (required): Display name for the token
900+
- `resources` (optional): Array of resource permissions to scope the token to. Each entry has:
901+
- `resource_type` (required): One of `account`, `project`, `inbox`, `domain`, `billing`
902+
- `resource_id` (required): ID of the resource
903+
- `access_level` (required): `100` (admin) or `10` (viewer)
904+
905+
### get-api-token
906+
907+
Get an API token by ID. Returns metadata only — the secret token value is **not** returned here (only from `create-api-token` / `reset-api-token`).
908+
909+
**Parameters:**
910+
911+
- `api_token_id` (required): ID of the API token
912+
913+
### reset-api-token
914+
915+
Reset (rotate) an API token by ID. The response includes the **new** secret `token` value — returned only on this call, so store it immediately. The previous token is invalidated.
916+
917+
**Parameters:**
918+
919+
- `api_token_id` (required): ID of the API token to reset
920+
921+
### delete-api-token
922+
923+
Permanently delete an API token by ID. The token can no longer authenticate after deletion.
924+
925+
**Parameters:**
926+
927+
- `api_token_id` (required): ID of the API token to delete
928+
830929
## Development
831930

832931
1. Clone the repository:

src/server.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,30 @@ import {
164164
getContactExport,
165165
getContactExportSchema,
166166
} from "./tools/contactExports";
167+
import { listAccounts, listAccountsSchema } from "./tools/accounts";
168+
import { getBillingUsage, getBillingUsageSchema } from "./tools/billing";
169+
import {
170+
listAccountAccesses,
171+
listAccountAccessesSchema,
172+
removeAccountAccess,
173+
removeAccountAccessSchema,
174+
} from "./tools/accountAccesses";
175+
import {
176+
getPermissionResources,
177+
getPermissionResourcesSchema,
178+
bulkUpdatePermissions,
179+
bulkUpdatePermissionsSchema,
180+
} from "./tools/permissions";
181+
import {
182+
listApiTokens,
183+
listApiTokensSchema,
184+
createApiToken,
185+
createApiTokenSchema,
186+
apiTokenSchema,
187+
getApiToken,
188+
resetApiToken,
189+
deleteApiToken,
190+
} from "./tools/apiTokens";
167191

168192
// Define the tools registry
169193
const tools = [
@@ -841,6 +865,115 @@ const tools = [
841865
readOnlyHint: true,
842866
},
843867
},
868+
{
869+
name: "list-accounts",
870+
description:
871+
"List Mailtrap accounts accessible to the API token, with each account's access levels.",
872+
inputSchema: listAccountsSchema,
873+
handler: listAccounts,
874+
annotations: {
875+
readOnlyHint: true,
876+
},
877+
},
878+
{
879+
name: "get-billing-usage",
880+
description:
881+
"Get the current billing cycle usage for the account (sending and testing plans, limits, and current counts).",
882+
inputSchema: getBillingUsageSchema,
883+
handler: getBillingUsage,
884+
annotations: {
885+
readOnlyHint: true,
886+
},
887+
},
888+
{
889+
name: "list-account-accesses",
890+
description:
891+
"List account accesses (users, invites, API tokens) for the account. Optionally scope by domain UUIDs, inbox IDs, or project IDs. Requires account admin/owner permissions.",
892+
inputSchema: listAccountAccessesSchema,
893+
handler: listAccountAccesses,
894+
annotations: {
895+
readOnlyHint: true,
896+
},
897+
},
898+
{
899+
name: "remove-account-access",
900+
description:
901+
"Remove an account access by ID. For User specifiers this revokes permissions; for Invite or ApiToken specifiers it removes the specifier itself. Requires admin/owner.",
902+
inputSchema: removeAccountAccessSchema,
903+
handler: removeAccountAccess,
904+
annotations: {
905+
destructiveHint: true,
906+
},
907+
},
908+
{
909+
name: "get-permission-resources",
910+
description:
911+
"Get all resources (inboxes, projects, domains, billing, account) to which the API token has admin access, nested by hierarchy.",
912+
inputSchema: getPermissionResourcesSchema,
913+
handler: getPermissionResources,
914+
annotations: {
915+
readOnlyHint: true,
916+
},
917+
},
918+
{
919+
name: "bulk-update-permissions",
920+
description:
921+
"Bulk create, update, or destroy permissions for an account access. Existing (resource_type, resource_id) pairs are updated; new ones are created. Set `destroy: true` to remove.",
922+
inputSchema: bulkUpdatePermissionsSchema,
923+
handler: bulkUpdatePermissions,
924+
annotations: {
925+
destructiveHint: true,
926+
},
927+
},
928+
{
929+
name: "list-api-tokens",
930+
description: "List all API tokens for the account.",
931+
inputSchema: listApiTokensSchema,
932+
handler: listApiTokens,
933+
annotations: {
934+
readOnlyHint: true,
935+
},
936+
},
937+
{
938+
name: "create-api-token",
939+
description:
940+
"Create a new API token. The response includes the secret `token` value — this is the **only time** the full token is returned, so store it immediately.",
941+
inputSchema: createApiTokenSchema,
942+
handler: createApiToken,
943+
annotations: {
944+
destructiveHint: true,
945+
},
946+
},
947+
{
948+
name: "get-api-token",
949+
description:
950+
"Get an API token by ID. The secret token value is NOT returned here — only the metadata (name, last 4 digits, resources).",
951+
inputSchema: apiTokenSchema,
952+
handler: getApiToken,
953+
annotations: {
954+
readOnlyHint: true,
955+
},
956+
},
957+
{
958+
name: "reset-api-token",
959+
description:
960+
"Reset (rotate) an API token by ID. The response includes the **new** secret `token` value — returned only on this call, so store it immediately. The previous token is invalidated.",
961+
inputSchema: apiTokenSchema,
962+
handler: resetApiToken,
963+
annotations: {
964+
destructiveHint: true,
965+
},
966+
},
967+
{
968+
name: "delete-api-token",
969+
description:
970+
"Permanently delete an API token by ID. The token can no longer authenticate after deletion.",
971+
inputSchema: apiTokenSchema,
972+
handler: deleteApiToken,
973+
annotations: {
974+
destructiveHint: true,
975+
},
976+
},
844977
];
845978

846979
export function createServer(): Server {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import listAccountAccesses from "../listAccountAccesses";
2+
import { requireClient } from "../../../client";
3+
4+
const mockClient = {
5+
general: {
6+
accountAccesses: {
7+
listAccountAccesses: jest.fn(),
8+
},
9+
},
10+
};
11+
12+
jest.mock("../../../client", () => ({
13+
requireClient: jest.fn(() => mockClient),
14+
}));
15+
16+
describe("listAccountAccesses", () => {
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
(requireClient as jest.Mock).mockReturnValue(mockClient);
20+
});
21+
22+
it("calls the SDK without filters when none provided", async () => {
23+
mockClient.general.accountAccesses.listAccountAccesses.mockResolvedValue([
24+
{ id: 1, name: "alice@example.com" },
25+
]);
26+
27+
const result = await listAccountAccesses();
28+
29+
expect(requireClient).toHaveBeenCalledWith("account accesses");
30+
expect(
31+
mockClient.general.accountAccesses.listAccountAccesses
32+
).toHaveBeenCalledWith(undefined);
33+
expect(result.content[0].text).toContain('"id": 1');
34+
expect(result.isError).toBeUndefined();
35+
});
36+
37+
it("translates snake_case filters to SDK camelCase", async () => {
38+
mockClient.general.accountAccesses.listAccountAccesses.mockResolvedValue(
39+
[]
40+
);
41+
42+
await listAccountAccesses({
43+
domain_uuids: ["d-1"],
44+
inbox_ids: ["i-2"],
45+
project_ids: ["p-3"],
46+
});
47+
48+
expect(
49+
mockClient.general.accountAccesses.listAccountAccesses
50+
).toHaveBeenCalledWith({
51+
domainUuids: ["d-1"],
52+
inboxIds: ["i-2"],
53+
projectIds: ["p-3"],
54+
});
55+
});
56+
57+
it("rejects invalid input before calling the SDK", async () => {
58+
const result = await listAccountAccesses({ domain_uuids: "not-an-array" });
59+
60+
expect(result.isError).toBe(true);
61+
expect(result.content[0].text).toContain(
62+
"Failed to list account accesses: Invalid input:"
63+
);
64+
expect(result.content[0].text).toContain("domain_uuids");
65+
expect(
66+
mockClient.general.accountAccesses.listAccountAccesses
67+
).not.toHaveBeenCalled();
68+
});
69+
70+
it("surfaces API errors", async () => {
71+
mockClient.general.accountAccesses.listAccountAccesses.mockRejectedValue(
72+
new Error("forbidden")
73+
);
74+
75+
const result = await listAccountAccesses();
76+
77+
expect(result.isError).toBe(true);
78+
expect(result.content[0].text).toBe(
79+
"Failed to list account accesses: forbidden"
80+
);
81+
});
82+
});

0 commit comments

Comments
 (0)