Skip to content
Merged
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
5 changes: 2 additions & 3 deletions LICENSING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ All content that resides under any directory named `ee` (e.g.
[`packages/backend/src/ee/`](packages/backend/src/ee/)) is **not**
licensed under the AGPL. It is licensed under the AnythingMCP
Commercial License — see the [LICENSE](packages/backend/src/ee/LICENSE)
file inside that directory. EE code contains operator-only
functionality for the AnythingMCP Cloud offering and is inert in
self-hosted deployments.
file inside that directory. EE code contains the AnythingMCP Cloud
operation and the licensing of the Business edition.

## Earlier releases

Expand Down
2 changes: 1 addition & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ curl -s http://localhost:4000/api/mcp-api-keys \
| `NEXTAUTH_SECRET` | No | NextAuth secret for frontend |
| `FRONTEND_URL` | No | Frontend URL for email links (default: `http://localhost:3000`) |
| `MCP_AUTH_MODE` | No | MCP auth: `none`, `legacy`, `oauth2`, `both` (default: `oauth2`) |
| `DEPLOYMENT_MODE` | No | `self-hosted` (default) or `cloud`. Anything other than `cloud` is self-hosted, so a community operator who never sets it keeps every self-hosted feature — including [single sign-on](sso.md) |
| `DEPLOYMENT_MODE` | No | `self-hosted` (default) or `cloud`. Anything other than `cloud` is self-hosted, so an operator who never sets it keeps every self-hosted feature — including [single sign-on](sso.md) |
| `MCP_BEARER_TOKEN` | No | Bearer token for legacy MCP auth |
| `MCP_API_KEY` | No | API key for legacy MCP auth |
| `SERVER_URL` | No | Server URL for OAuth2 metadata (default: `http://localhost:4000`) |
Expand Down
11 changes: 8 additions & 3 deletions docs/license-faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

AnythingMCP is licensed under the **GNU Affero General Public License v3** (AGPL-3.0-only), an OSI-approved **open-source** license. It is the same license used by Twenty, Cal.com, Grafana, Plausible and Mastodon.

The only exception is code under `ee/` directories (e.g. `packages/backend/src/ee/`), which contains operator-only functionality for the AnythingMCP Cloud offering and is licensed under the [AnythingMCP Commercial License](../packages/backend/src/ee/LICENSE). EE code is inert in self-hosted deployments — you don't need it.
The only exception is code under `ee/` directories (e.g. `packages/backend/src/ee/`), which contains the AnythingMCP Cloud operation and the licensing of the Business edition, and is licensed under the [AnythingMCP Commercial License](../packages/backend/src/ee/LICENSE).

---

Expand Down Expand Up @@ -50,16 +50,21 @@ You can *use* AnythingMCP from a proprietary product over its API (your product

## What is the `ee/` directory?

Code under `ee/` directories powers the AnythingMCP Cloud operation (e.g. onboarding lifecycle emails). It is:
Code under `ee/` directories powers the AnythingMCP Cloud operation (e.g. onboarding lifecycle emails) and decides which edition a self-hosted instance runs. It is:

- **Visible** — you can read and audit it like the rest of the repo
- **Not AGPL** — it's under the AnythingMCP Commercial License
- **Not needed for self-hosting** — EE modules only load when `DEPLOYMENT_MODE=cloud`

This split (AGPL core + commercial `ee/`) is the same model used by Cal.com and GitLab.

---

## Community and Business

A self-hosted instance runs **Community** unless a licence key is activated. Community includes up to 3 active users. **Business** adds more users, single sign-on (Entra ID, Google, Okta, OIDC) and SCIM provisioning; administrators can try it for 30 days under **Settings → License**. See [anythingmcp.com/pricing](https://anythingmcp.com/pricing).

---

## What about old releases?

Versions of AnythingMCP released **before** the AGPL adoption remain under the **Business Source License 1.1** they were published with. Those releases convert automatically to Apache 2.0 on their Change Date (2030-03-04). Everything from the AGPL adoption onward is AGPL-3.0-only.
Expand Down
3 changes: 2 additions & 1 deletion docs/scim-entra-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ handled, how roles behave between sign-ins — read
mechanics.

> **Self-hosted only.** Every SCIM route answers **404** on AnythingMCP Cloud.
> See [deployment.md](deployment.md) to run your own instance.
> See [deployment.md](deployment.md) to run your own instance. Provisioning
> new users needs AnythingMCP Business (see [the SSO guide](sso.md)).

**Time:** about 15 minutes. **Reversible:** yes, at every step.

Expand Down
3 changes: 3 additions & 0 deletions docs/sso.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ password, and keep their AnythingMCP roles in step with your directory groups.
> in a shared, multi-tenant deployment it would let any customer point a
> workspace at an arbitrary directory and provision accounts from it. Run your
> own instance (Docker) to use it — see [deployment.md](deployment.md).
>
> Setting up SSO and SCIM needs **AnythingMCP Business**: a licence key, or
> the 30-day trial an administrator can start under **Settings → License**.

---

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module, MiddlewareConsumer, NestModule, Logger, ClassSerializerInterceptor } from '@nestjs/common';

Check warning on line 1 in packages/backend/src/app.module.ts

View workflow job for this annotation

GitHub Actions / Backend (lint, typecheck, test, build)

'ClassSerializerInterceptor' is defined but never used. Allowed unused vars must match /^_/u
import { ConfigModule, ConfigService } from '@nestjs/config';
import { join } from 'path';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
Expand Down Expand Up @@ -39,6 +39,7 @@
import { AdaptersModule } from './adapters/adapters.module';
import { OrganizationsModule } from './organizations/organizations.module';
import { CloudModule } from './ee/cloud/cloud.module';
import { LicensingModule } from './ee/licensing/licensing.module';
import { getRequiredSecret } from './common/secrets.util';
import { AppLoggerModule } from './common/logger.module';
import { SentryContextInterceptor } from './common/sentry-context.interceptor';
Expand Down Expand Up @@ -148,6 +149,7 @@
KgModule,
McpServersModule,
LicenseModule,
LicensingModule,

// Cloud-specific modules (conditionally loaded)
...cloudImports,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ function makeController({
ssoEnforcement as any,
// The real service, so the test sees what would actually be stored.
new ProductEventService(prisma as any),
{ assertSeatAvailable: jest.fn(async () => undefined), getState: jest.fn(async () => ({ trialAvailable: true })) } as any, // edition
);
return { controller, users, sent, events, authService, usersService, emailService, prisma };
}
Expand Down
13 changes: 12 additions & 1 deletion packages/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { RecoveryCodesService } from './recovery-codes.service';
import { SsoEnforcementService } from './sso-enforcement.service';
import { Roles, RolesGuard } from './roles.guard';
import { SelfHostedOnlyGuard } from '../common/self-hosted-only.guard';
import { EditionService } from '../ee/licensing/edition.service';
import { SignupAttributionDto } from './signup-attribution.dto';

/**
Expand Down Expand Up @@ -223,6 +224,7 @@ export class AuthController {
private readonly recoveryCodes: RecoveryCodesService,
private readonly ssoEnforcement: SsoEnforcementService,
private readonly productEvents: ProductEventService,
private readonly edition: EditionService,
) {}

private getFrontendUrl(_req?: any): string {
Expand Down Expand Up @@ -398,7 +400,10 @@ export class AuthController {
let needsLicenseSetup = false;
if (user.role === 'ADMIN') {
const licenseKey = await this.siteSettings.get('license_key');
if (!licenseKey) {
// Self-hosted: once the Business trial has been started the choice has
// been made, and the chooser would only offer a trial that cannot start.
const isCloud = this.configService.get<string>('DEPLOYMENT_MODE') === 'cloud';
if (!licenseKey && (isCloud || (await this.edition.getState()).trialAvailable)) {
needsLicenseSetup = true;
}
}
Expand Down Expand Up @@ -445,6 +450,7 @@ export class AuthController {
if (existing) {
throw new ConflictException('Email already registered');
}
if (userCount > 0) await this.edition.assertSeatAvailable();

// Self-hosted: first user is ADMIN of a new organization, later ones
// join the existing (first) organization as EDITOR.
Expand Down Expand Up @@ -721,6 +727,9 @@ export class AuthController {
}
// User exists but not in this org — allow invitation for multi-org membership
}
// Refused here too, not only on acceptance, so the admin learns it before
// the invitee does.
await this.edition.assertSeatAvailable(existing?.id);

// 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 @@ -852,11 +861,13 @@ export class AuthController {
: 'You are already a member of this organization',
);
}
await this.edition.assertSeatAvailable(existing.id);
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);
} else {
// New user — create account and join the organization
await this.edition.assertSeatAvailable();
const passwordHash = await this.authService.hashPassword(dto.password);
user = await this.usersService.create({
email: invite.email,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/auth/signup-attribution.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ function makeController(mode: 'cloud' | 'self-hosted', existingEmails: string[]
{} as any,
{} as any,
new ProductEventService(prisma as any),
{ assertSeatAvailable: jest.fn(async () => undefined), getState: jest.fn(async () => ({ trialAvailable: true })) } as any, // edition
);
return { controller, events, usersService };
}
Expand Down
10 changes: 6 additions & 4 deletions packages/backend/src/ee/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ Without a Commercial Agreement you may:
1. View and audit the EE Code.
2. Modify the EE Code solely for the purpose of contributing changes
back to helpcode.ai GmbH.
3. Build and run the software with the EE Code disabled (the default
for self-hosted deployments — EE modules are only loaded when
DEPLOYMENT_MODE=cloud or an equivalent operator flag is set).
3. Build and run the software with the EE Code's commercial
functionality inactive, which is the default for self-hosted
deployments without a licence key.

Without a Commercial Agreement you may NOT:

1. Run the EE Code in production.
1. Use the EE Code's commercial functionality in production without
a valid licence key or Commercial Agreement, other than during the
built-in trial or the transition period of an upgraded instance.
2. Redistribute the EE Code, modified or unmodified, as part of any
product or service.
3. Remove, bypass, or alter any license-checking or feature-gating
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/ee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ Everything under this directory is licensed under the [AnythingMCP
Commercial License](LICENSE), **not** the AGPL that covers the rest of
the repository.

EE code contains operator-only functionality used by the AnythingMCP
Cloud offering (e.g. onboarding lifecycle emails). It is inert in
self-hosted deployments: EE modules are only loaded when
`DEPLOYMENT_MODE=cloud`, and a community deployment works fully
without them.
- `cloud/` — operator-only functionality of AnythingMCP Cloud (e.g.
onboarding lifecycle emails). Loaded only when `DEPLOYMENT_MODE=cloud`.
- `licensing/` — which edition a self-hosted instance runs (Community
or Business) and what each allows. Loaded everywhere; Business
capabilities are active only with a licence key, during the trial, or
during the transition period of an upgraded instance.

If you are self-hosting, you don't need anything in here. If you want
to use EE features commercially, contact info@helpcode.ai.
For licensing questions, contact info@helpcode.ai.
27 changes: 27 additions & 0 deletions packages/backend/src/ee/licensing/business-edition.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CanActivate, ExecutionContext, Injectable, SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { EditionService } from './edition.service';

const CAPABILITY_KEY = 'business_capability';

/** Names the capability in the 403 message, e.g. `@BusinessCapability('Single sign-on')`. */
export const BusinessCapability = (name: string) => SetMetadata(CAPABILITY_KEY, name);

/** Lets a route through only when the instance has Business capabilities. */
@Injectable()
export class BusinessEditionGuard implements CanActivate {
constructor(
private readonly edition: EditionService,
private readonly reflector: Reflector,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const capability =
this.reflector.getAllAndOverride<string>(CAPABILITY_KEY, [
context.getHandler(),
context.getClass(),
]) ?? 'This feature';
await this.edition.assertBusiness(capability);
return true;
}
}
28 changes: 28 additions & 0 deletions packages/backend/src/ee/licensing/edition.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Controller, Get, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { Roles, RolesGuard } from '../../auth/roles.guard';
import { EditionService } from './edition.service';

@ApiTags('License')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Controller('api/license')
export class EditionController {
constructor(private readonly edition: EditionService) {}

@Get('edition')
@ApiOperation({ summary: 'Edition of this instance, its user limit and Business availability' })
getEdition() {
return this.edition.getState();
}

@Post('business-trial')
@HttpCode(HttpStatus.OK)
@UseGuards(RolesGuard)
@Roles('ADMIN')
@ApiOperation({ summary: 'Start the one-time Business trial on a self-hosted instance (ADMIN)' })
startTrial() {
return this.edition.startTrial();
}
}
Loading
Loading