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
8 changes: 2 additions & 6 deletions packages/backend/src/auth/auth-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,14 @@ export function clientTilePair(clientName: string, redirectHost: string): string
export interface TrustRowOptions {
/** Cloud-only claims (Frankfurt, DPA) are shown only on AnythingMCP Cloud. */
cloud: boolean;
/** GitHub stars, formatted ("984", "1,100+"), or null when unknown. */
stars: string | null;
}

export function trustRow(opts: TrustRowOptions): string {
const items = [
`<span>${LOCK_SVG} AES-256-GCM encryption</span>`,
...(opts.cloud ? ['<span>&#127466;&#127482; Hosted in Frankfurt</span>', '<span>GDPR &middot; DPA</span>'] : []),
'<span>Every call in your audit log</span>',
opts.stars
? `<span class="gh"><span class="star">&#9733;</span> ${escapeHtml(opts.stars)} on GitHub &middot; open source</span>`
: '<span class="gh">Open source on GitHub</span>',
'<span class="gh">Open source on GitHub</span>',
];
return `<div class="trust">${items.join('')}</div>
<div class="foot">AnythingMCP by helpcode.ai GmbH, Freiburg, Germany</div>`;
Expand Down Expand Up @@ -189,7 +185,7 @@ const STYLES = `
.trust { max-width: 440px; width: 100%; margin-top: 18px; display: flex; flex-wrap: wrap; justify-content: center;
gap: 8px 16px; font-size: 12px; color: var(--text-3); }
.trust span { display: inline-flex; align-items: center; gap: 6px; }
.trust .gh { color: var(--text-2); font-weight: 500; } .trust .star { color: #e3a008; }
.trust .gh { color: var(--text-2); font-weight: 500; }
.foot { margin-top: 10px; font-size: 11.5px; color: var(--text-3); text-align: center; }
@media (max-width: 480px) {
body { padding: 16px 12px 24px; justify-content: flex-start; }
Expand Down
15 changes: 6 additions & 9 deletions packages/backend/src/auth/login.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,7 @@ describe('LoginController', () => {
cloud: { mode: 'cloud', isCloud: () => true, isSelfHosted: () => false },
selfHosted: { mode: 'self-hosted', isCloud: () => false, isSelfHosted: () => true },
};
const make = (deployment: object, p: object = prisma, trustStats?: object) =>
const make = (deployment: object, p: object = prisma) =>
new LoginController(
authService as unknown as AuthService,
p as unknown as PrismaService,
Expand All @@ -755,9 +755,6 @@ describe('LoginController', () => {
sso as unknown as SsoService,
deployment as any,
grants as any,
undefined,
undefined,
trustStats as any,
);
const session = (redirectUri: string, clientName: string) => {
store.getOAuthSession.mockResolvedValue({
Expand Down Expand Up @@ -846,14 +843,14 @@ describe('LoginController', () => {
expect(html.indexOf('value="approve"')).toBeLessThan(html.indexOf('value="deny"'));
});

it('makes Frankfurt and DPA claims on Cloud only, with the live star count', async () => {
it('makes Frankfurt and DPA claims on Cloud only, and shows no star count', async () => {
session('https://claude.ai/api/mcp/auth_callback', 'Claude');
const stats = { peek: () => ({ githubStars: 1234 }) };
const cloudHtml = await page(make(DEPLOY.cloud, prisma, stats));
const cloudHtml = await page(make(DEPLOY.cloud, prisma));
expect(cloudHtml).toContain('Hosted in Frankfurt');
expect(cloudHtml).toContain('GDPR &middot; DPA');
expect(cloudHtml).toContain('1,200+ on GitHub');
const selfHtml = await page(make(DEPLOY.selfHosted, prisma, stats));
expect(cloudHtml).toContain('Open source on GitHub');
expect(cloudHtml).not.toContain('&#9733;');
const selfHtml = await page(make(DEPLOY.selfHosted, prisma));
expect(selfHtml).not.toMatch(/Frankfurt|DPA/);
expect(selfHtml).toContain('AES-256-GCM');
});
Expand Down
13 changes: 2 additions & 11 deletions packages/backend/src/auth/login.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import { providerMarkSvg } from './provider-marks';
import { McpConnectionGrantService } from '../mcp-servers/mcp-connection-grant.service';
import { ProductEvents, ProductEventService } from '../audit/product-event.service';
import { LicenseGuardService } from '../license/license-guard.service';
import { TrustStatsService } from '../public-stats/trust-stats.service';
import { formatStars } from '../public-stats/trust-stats.format';
import {
AMCP_MARK_SVG as AMCP_MARK,
claudeMark,
Expand Down Expand Up @@ -96,18 +94,11 @@ export class LoginController {
private readonly grants: McpConnectionGrantService,
@Optional() private readonly productEvents?: ProductEventService,
@Optional() private readonly licenseGuard?: LicenseGuardService,
@Optional() private readonly trustStats?: TrustStatsService,
) {}

/** The trust row under every page of the flow. Never waits on GitHub. */
/** The trust row under every page of the flow. */
private trust(): TrustRowOptions {
let stars: string | null = null;
try {
stars = formatStars(this.trustStats?.peek().githubStars);
} catch {
stars = null;
}
return { cloud: this.deployment.isCloud(), stars };
return { cloud: this.deployment.isCloud() };
}

/**
Expand Down
26 changes: 10 additions & 16 deletions packages/backend/src/public-stats/trust-stats.format.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/**
* The public trust numbers (GitHub stars, Docker pulls, Cloud workspaces, AI
* tool calls in the last 30 days) and how they are shown. Stars, downloads and
* tool calls are never shown below TRUST_FLOORS; above a floor the live number
* is shown, rounded DOWN. Workspaces are live only: a value below its rounding
* tool calls in the last 30 days) and how they are shown. Downloads and tool
* calls are never shown below TRUST_FLOORS; above a floor the live number is
* shown, rounded DOWN. Workspaces are live only: a value below its rounding
* unit is not shown at all rather than as "0+".
*
* GitHub stars are still collected and served by /api/public/stats, but shown
* nowhere: Matteo took them off the website, the emails and the sign-in pages
* on 10 Oct 2026.
*/
export interface TrustStats {
githubStars: number | null;
Expand All @@ -15,8 +19,6 @@ export interface TrustStats {
}

export interface TrustStatsDisplay {
/** "984" below 1,000, then "1,000+", "1,100+". */
stars: string | null;
/** "32,000+". */
downloads: string | null;
/** "3,800+". */
Expand All @@ -26,12 +28,11 @@ export interface TrustStatsDisplay {
}

/**
* The least each figure is shown as (Matteo, 8 Oct 2026): 1,000+ stars,
* 200,000+ downloads across all distribution channels, 1M+ AI tool calls last
* month. Docker Hub alone undercounts downloads, so the floor applies even
* The least each figure is shown as (Matteo, 8 Oct 2026): 200,000+ downloads
* across all distribution channels, 1M+ AI tool calls last month. Docker Hub alone undercounts downloads, so the floor applies even
* when a live number is missing or lower.
*/
export const TRUST_FLOORS = { githubStars: 1_000, dockerPulls: 200_000, toolCalls30d: 1_000_000 } as const;
export const TRUST_FLOORS = { dockerPulls: 200_000, toolCalls30d: 1_000_000 } as const;

const atLeast = (n: number | null | undefined, floor: number) => (usable(n) && n > floor ? n : floor);

Expand All @@ -54,12 +55,6 @@ function floorPlus(n: number | null | undefined, unit: number): string | null {
return `${thousands(Math.floor(n / unit) * unit)}+`;
}

export function formatStars(n: number | null | undefined): string | null {
if (!usable(n)) return null;
const whole = Math.floor(n);
return whole < 1000 ? thousands(whole) : floorPlus(whole, 100);
}

export function formatDownloads(n: number | null | undefined): string | null {
return floorPlus(n, 1000);
}
Expand All @@ -76,7 +71,6 @@ export function formatToolCalls(n: number | null | undefined): string | null {

export function formatTrustStats(stats: TrustStats | null | undefined): TrustStatsDisplay {
return {
stars: formatStars(atLeast(stats?.githubStars, TRUST_FLOORS.githubStars)),
downloads: formatDownloads(atLeast(stats?.dockerPulls, TRUST_FLOORS.dockerPulls)),
workspaces: formatWorkspaces(stats?.workspaces),
toolCalls: formatToolCalls(atLeast(stats?.toolCalls30d, TRUST_FLOORS.toolCalls30d)),
Expand Down
19 changes: 3 additions & 16 deletions packages/backend/src/public-stats/trust-stats.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import axios from 'axios';
import {
formatDownloads,
formatStars,
formatToolCalls,
formatTrustStats,
formatWorkspaces,
Expand All @@ -13,18 +12,6 @@ jest.mock('axios');
const mockedGet = axios.get as jest.Mock;

describe('trust number formatting (always rounded down)', () => {
it.each([
[984, '984'],
[999, '999'],
[1000, '1,000+'],
[1099, '1,000+'],
[1100, '1,100+'],
[12_345, '12,300+'],
[0, null],
[null, null],
[Number.NaN, null],
])('stars %p -> %p', (n, out) => expect(formatStars(n as any)).toBe(out));

it.each([
[32_456, '32,000+'],
[999, null],
Expand All @@ -48,11 +35,11 @@ describe('trust number formatting (always rounded down)', () => {
it('formats a whole set, never below the floors', () => {
expect(
formatTrustStats({ githubStars: 984, dockerPulls: 32_456, workspaces: 3_871, toolCalls30d: 654_321, updatedAt: null }),
).toEqual({ stars: '1,000+', downloads: '200,000+', workspaces: '3,800+', toolCalls: '1M+' });
).toEqual({ downloads: '200,000+', workspaces: '3,800+', toolCalls: '1M+' });
expect(
formatTrustStats({ githubStars: 1_234, dockerPulls: 250_400, workspaces: null, toolCalls30d: 2_100_000, updatedAt: null }),
).toEqual({ stars: '1,200+', downloads: '250,000+', workspaces: null, toolCalls: '2M+' });
expect(formatTrustStats(null)).toEqual({ stars: '1,000+', downloads: '200,000+', workspaces: null, toolCalls: '1M+' });
).toEqual({ downloads: '250,000+', workspaces: null, toolCalls: '2M+' });
expect(formatTrustStats(null)).toEqual({ downloads: '200,000+', workspaces: null, toolCalls: '1M+' });
});
});

Expand Down
11 changes: 1 addition & 10 deletions packages/backend/src/settings/email-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export const EMAIL_COLORS = {
muted: '#6b665c',
blue: '#2159e0',
tint: '#eef2fd',
star: '#e3a008',
} as const;

const C = EMAIL_COLORS;
Expand Down Expand Up @@ -193,13 +192,11 @@ const CLIENTS: Array<{ file: string; alt: string; label: string }> = [
function trustBand(ctx: EmailBrandContext, assetBase: string): string {
const { stats, cloud } = ctx;
const tiles: Array<{ big: string; small: string }> = [];
if (stats.stars) {
tiles.push({ big: `<span style="color:${C.star};">&#9733;</span> ${esc(stats.stars)}`, small: 'stars on GitHub' });
}
if (stats.downloads) tiles.push({ big: esc(stats.downloads), small: 'downloads' });
// The tool-call count is AnythingMCP Cloud's. A self-hosted instance only
// knows its own, which would read as if it were everyone's.
if (cloud && stats.toolCalls) tiles.push({ big: esc(stats.toolCalls), small: 'AI tool calls last month' });
if (cloud && stats.workspaces) tiles.push({ big: esc(stats.workspaces), small: 'workspaces on AnythingMCP Cloud' });
const width = tiles.length ? Math.floor(100 / tiles.length) : 100;
const statRow = tiles.length
? `<tr><td style="padding:0 8px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>
Expand All @@ -212,10 +209,6 @@ ${tiles
.join('\n')}
</tr></table></td></tr>`
: '';
const workspaces =
cloud && stats.workspaces
? `<tr><td style="padding:0 16px 6px 16px;text-align:center;font-family:${SANS};font-size:12px;color:${C.muted};"><span style="color:${C.ink};font-weight:600;">${esc(stats.workspaces)}</span> workspaces on AnythingMCP Cloud</td></tr>`
: '';
const directory = cloud
? `<a href="${CLAUDE_DIRECTORY_URL}" style="text-decoration:none;color:${C.ink};">${pill(
`<img src="${esc(assetBase)}/claude.png" width="12" height="12" alt="" style="vertical-align:-2px;border:0;"> Listed in the Claude Directory`,
Expand All @@ -235,7 +228,6 @@ ${tiles
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#ffffff;border:1px solid ${C.border};border-radius:14px;">
<tr><td style="padding:20px 24px 4px 24px;font-family:${SANS};font-size:13px;font-weight:600;color:${C.muted};text-align:center;">Why teams build on AnythingMCP</td></tr>
${statRow}
${workspaces}
<tr><td style="padding:4px 16px 12px 16px;text-align:center;line-height:2.4;">
${badges}
</td></tr>
Expand Down Expand Up @@ -301,7 +293,6 @@ export function renderEmailText(input: {
}): string {
const { stats, cloud } = input.ctx;
const numbers = [
stats.stars ? `${stats.stars} stars on GitHub` : null,
stats.downloads ? `${stats.downloads} downloads` : null,
cloud && stats.toolCalls ? `${stats.toolCalls} AI tool calls last month` : null,
cloud && stats.workspaces ? `${stats.workspaces} workspaces on AnythingMCP Cloud` : null,
Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/settings/email-templates.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,16 @@ describe('trust band', () => {

it('shows the numbers, never below their floors', () => {
const html = verify.render(cloud, { name: 'A', other: 'B' }).html;
expect(html).toContain('1,000+');
expect(html).toContain('200,000+');
expect(html).toContain('1M+');
expect(html).toContain('3,800+');
expect(html).toContain('workspaces on AnythingMCP Cloud');
expect(html).not.toContain('stars on GitHub');
});

it('shows the floors without live numbers, and no workspace count', () => {
const html = verify.render(noStats, { name: 'A', other: 'B' }).html;
expect(html).toContain('stars on GitHub');
expect(html).not.toContain('stars on GitHub');
expect(html).toContain('200,000+');
expect(html).not.toContain('workspaces on');
expect(html).not.toMatch(/\b0\+/);
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/settings/email.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ describe('EmailService', () => {
expect(mail.text).toContain('123456');
expect(mail.headers).toBeUndefined();
expect(mail.html).not.toContain('Unsubscribe');
// A live number above its floor, rounded down; the others at their floors.
expect(mail.html).toContain('1,200+');
// Downloads and tool calls at their floors; no GitHub star count.
expect(mail.html).not.toContain('stars on GitHub');
expect(mail.html).toContain('200,000+');
expect(mail.html).toContain('1M+');
});
Expand Down Expand Up @@ -145,6 +145,6 @@ describe('EmailService', () => {
trustStats.get.mockRejectedValue(new Error('down'));
trustStats.peek.mockReturnValue({ githubStars: null, dockerPulls: null, workspaces: null, toolCalls30d: null, updatedAt: null });
await expect(service.sendExistingAccountEmail('a@b.com', 'https://cloud.anythingmcp.com/login', 'https://cloud.anythingmcp.com/forgot-password')).resolves.toBe(true);
expect(sent().html).toContain('1,000+');
expect(sent().html).toContain('200,000+');
});
});
2 changes: 1 addition & 1 deletion packages/backend/src/settings/unsubscribe.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class UnsubscribeController {
renderAuthPage({
title: 'Email preferences — AnythingMCP',
card,
trust: { cloud: this.deployment.isCloud(), stars: null },
trust: { cloud: this.deployment.isCloud() },
}),
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@
if (offerCardTrial(storedUser, trialResult)) return;
setTrialDaysLeft(trialResult.trialDaysLeft);
setSetupStep('trial-activated');
} catch (trialErr: any) {

Check warning on line 381 in packages/frontend/src/app/login/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend (lint, typecheck, build)

'trialErr' is defined but never used
// Trial may already exist (e.g. returning user) — go to dashboard
goTo(redirectTo);
}
Expand Down Expand Up @@ -1126,7 +1126,7 @@
return (
<div className={singleColumn}>
{formCard}
{infoLoaded && <TrustLine cloud={isCloudMode} stats={trustStats} />}
{infoLoaded && <TrustLine cloud={isCloudMode} />}
</div>
);
}
Expand Down
26 changes: 7 additions & 19 deletions packages/frontend/src/components/auth-trust.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ export function TrustPanel({
directoryBadge?: boolean;
}) {
const tiles = [
stats?.stars ? { n: stats.stars, l: 'stars on GitHub', star: true } : null,
stats?.downloads ? { n: stats.downloads, l: 'downloads' } : null,
stats?.toolCalls ? { n: stats.toolCalls, l: 'AI tool calls last month' } : null,
stats?.workspaces ? { n: stats.workspaces, l: 'workspaces on AnythingMCP Cloud' } : null,
].filter(Boolean) as Array<{ n: string; l: string; star?: boolean }>;
].filter(Boolean) as Array<{ n: string; l: string }>;

return (
<aside
Expand All @@ -60,15 +59,13 @@ export function TrustPanel({
</p>

{tiles.length > 0 && (
<div className="mb-6 grid grid-cols-2 gap-2 sm:gap-3" data-testid="trust-stats">
<div
className={`mb-6 grid ${tiles.length === 3 ? 'grid-cols-3' : 'grid-cols-2'} gap-2 sm:gap-3`}
data-testid="trust-stats"
>
{tiles.map((t) => (
<div key={t.l} className="rounded-[12px] border border-white/[.09] bg-white/[.05] p-2.5 sm:p-3.5">
<div className="text-[17px] font-semibold tracking-[-0.01em] sm:text-[22px]">
{t.star && (
<span aria-hidden className="text-[#f5b301]">
&#9733;{' '}
</span>
)}
{t.n}
</div>
<div className="mt-1 text-[11px] leading-snug text-[#97a1b5] sm:text-xs">{t.l}</div>
Expand Down Expand Up @@ -136,7 +133,7 @@ export function TrustPanel({
}

/** One quiet line under a sign-in card. */
export function TrustLine({ cloud, stats }: { cloud: boolean; stats: TrustStatsDisplay | null }) {
export function TrustLine({ cloud }: { cloud: boolean }) {
const items = [
'AES-256-GCM encryption',
...(cloud ? ['Hosted in Frankfurt', 'GDPR · DPA'] : []),
Expand All @@ -153,16 +150,7 @@ export function TrustLine({ cloud, stats }: { cloud: boolean; stats: TrustStatsD
rel="noopener noreferrer"
className="font-medium text-[var(--text-2)] hover:text-[var(--text)]"
>
{stats?.stars ? (
<>
<span aria-hidden className="text-[#e3a008]">
&#9733;
</span>{' '}
{stats.stars} on GitHub · open source
</>
) : (
'Open source on GitHub'
)}
Open source on GitHub
</a>
</div>
);
Expand Down
Loading
Loading