Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/cloud-plans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Cloud plan limits

This page applies to AnythingMCP Cloud only. A self-hosted instance has none of
these limits: there, the Community edition counts active users, and
Enterprise features follow the licence key (see [license FAQ](license-faq.md)).

## What each plan includes

| | Trial (7 days) | Starter | Team | Enterprise |
|---|---|---|---|---|
| Active connectors | 15 | 5 | 15 | unlimited |
| Active MCP servers | 10 | 3 | 10 | unlimited |
| Users | 3 | 1 | 3 | by contract |
| File Search | Team limits | 1 folder, 500 files | 3 folders, 2,000 files | by contract |
| Code mode | Team features | 5 saved code tools, no inline code | 20 saved code tools, inline code | unlimited |

The trial includes everything in Team, so you can try every feature before you
choose a plan. File Search and code mode have their own limits, described in
[File Search](connectors/file-search.md#limits-and-pauses) and
[code mode](code-mode.md#limits).

Only **active** connectors and MCP servers count. A File Search folder counts
against the File Search folders, not against the connectors.

## Before you choose a plan

When the trial ends, the plan picker shows what changes with each plan for
your workspace, for example "4 of your 9 connectors would be paused". Choose
**See which ones** to see which connectors would stay active and which would
be paused. Team is always one click away.

`GET /api/license/plan-fit` returns the same answer for scripts and the
[CLI](../packages/cli/README.md#check-what-a-plan-change-would-pause-cloud).

## When a plan includes fewer active items

When a workspace moves to a plan with lower limits (the trial to Starter, or
Team to Starter):

- the connectors and MCP servers used most in the last 30 days stay active;
- the others are **paused**. Nothing is deleted: credentials, tools and settings
are kept. Your AI clients no longer see the tools of a paused connector, and
the AnythingMCP tools tell the model which connectors are paused;
- the workspace admins get an email listing what was paused.

You choose what stays active at any time. Pause a connector, then switch on
another one. At the limit, switching one on asks you to pause another first.
Paused connectors show **Paused: plan limit** in the dashboard.

When you upgrade again, everything the downgrade paused is switched back on
automatically, up to the new plan's limits.

Members keep their access when the plan includes fewer users. New
invitations above the limit are refused.

Over a limit, the API answers `403` with `code: "PLAN_LIMIT"`, the `axis`
(`connectors`, `mcpServers`, `users` or `fileSearchConnectors`), `max` and
`current`.
1 change: 1 addition & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ curl -s http://localhost:4000/api/mcp-api-keys \
| `SMTP_PASS` | No | SMTP password |
| `SMTP_FROM` | No | Sender address (default: `AnythingMCP <SMTP_USER>`) |
| `SMTP_REPLY_TO` | No | Reply-To address, for when `SMTP_FROM` is a noreply address |
| `PLAN_CAPS_ENFORCE` | No | Cloud only: how hard the plans' caps on connectors, MCP servers and users are (see [Cloud plan limits](cloud-plans.md)). `dry-run` (default) logs what would be refused or paused, `on` enforces them, `off` does neither. The trial's cap on creating connectors and MCP servers is enforced in every mode. Ignored on self-hosted, which has none of these caps |
| `RETENTION_SMTP_HOST` | No | Cloud only: a second SMTP server for the trial lifecycle, win-back and blocked-call emails. Without it they go through the SMTP above, with `RETENTION_SMTP_REPLY_TO` as Reply-To when set. Self-hosted sends none of these emails |
| `RETENTION_SMTP_PORT` | No | Its port (default: 587). 465 or 2465 use implicit TLS, any other port STARTTLS |
| `RETENTION_SMTP_USER` | No | Its username |
Expand Down
1 change: 1 addition & 0 deletions llms-full.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PlanCapsModule } from './plan-caps/plan-caps.module';
import { Module, MiddlewareConsumer, NestModule, Logger, ClassSerializerInterceptor } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { join } from 'path';
Expand Down Expand Up @@ -182,6 +183,7 @@ if (useOAuth) {
McpServersModule,
FileSearchModule,
LicenseModule,
PlanCapsModule,
LicensingModule,

// Cloud-specific modules (conditionally loaded)
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/audit/product-event.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,13 @@ export const ProductEvents = {
* screen and device; read with checkout_link_created and Stripe.
*/
PAYWALL_ACTION: 'paywall_action',
/**
* Cloud: a plan change paused items above the new plan's caps
* (metadata.kind 'downgrade') or brought them back ('restore');
* metadata.plan = the new plan, metadata.calls = how many items,
* metadata.status = 'dry-run' or 'on' (PLAN_CAPS_ENFORCE). PlanCapsService.
*/
PLAN_CAPS_APPLIED: 'plan_caps_applied',
} as const;

/** The values of `action` on paywall_action (see PAYWALL_ACTION). */
Expand Down Expand Up @@ -371,6 +378,7 @@ export type ProductEventName = (typeof ProductEvents)[keyof typeof ProductEvents
*/
const SERVER_ONLY = new Set<string>([
ProductEvents.SIGNUP_ATTRIBUTED,
ProductEvents.PLAN_CAPS_APPLIED,
ProductEvents.OAUTH_FAILED,
ProductEvents.AI_CLIENT_CONNECTED,
ProductEvents.DIRECTORY_SIGNUP,
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LicenseGuardService } from '../license/license-guard.service';
import {
Controller,
Post,
Expand Down Expand Up @@ -236,6 +237,7 @@ export class AuthController {
private readonly productEvents: ProductEventService,
private readonly edition: EditionService,
@Optional() private readonly adsSignup?: AdsSignupService,
@Optional() private readonly licenseGuard?: LicenseGuardService,
) {
// Built from this controller's own dependencies (it is stateless), so the
// dashboard endpoints and the MCP authorization page share one
Expand Down Expand Up @@ -709,6 +711,8 @@ export class AuthController {
// Refused here too, not only on acceptance, so the admin learns it before
// the invitee does.
await this.edition.assertSeatAvailable(existing?.id);
// Cloud: the plan's user cap (PLAN_CAPS_ENFORCE).
await this.licenseGuard?.checkCanAddMember(req.user.organizationId);

// Reuse an existing pending invitation instead of rejecting: throwing a
// 409 here left admins stuck with no way to get the link after a failed
Expand Down Expand Up @@ -882,6 +886,7 @@ export class AuthController {
);
}
await this.edition.assertSeatAvailable(existing.id);
await this.licenseGuard?.checkCanAddMember(invite.organizationId);
await this.organizationsService.addMember(existing.id, invite.organizationId, invite.role);
// Switch their active org to the newly joined one
user = await this.organizationsService.switchOrg(existing.id, invite.organizationId);
Expand All @@ -896,6 +901,7 @@ export class AuthController {
throw new BadRequestException('A display name is required.');
}
await this.edition.assertSeatAvailable();
await this.licenseGuard?.checkCanAddMember(invite.organizationId);
const passwordHash = await this.authService.hashPassword(dto.password);
user = await this.usersService.create({
email: invite.email,
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/connectors/connectors.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,10 @@ export class ConnectorsController {
) {
const connector = await this.connectorsService.findById(id);
this.assertCanWrite(connector, req);
// Switching a paused connector back on counts against the plan's cap (Cloud).
if (dto.isActive === true && !connector.isActive) {
await this.licenseGuard.checkCanActivateConnector(connector.organizationId, connector.type as string);
}
if ((connector.type as string) === 'FILE_SEARCH' && (dto.config !== undefined || dto.baseUrl !== undefined || dto.authConfig !== undefined)) {
throw new BadRequestException('Change the source of a File Search connector on its Files tab.');
}
Expand Down
Loading
Loading