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
320 changes: 285 additions & 35 deletions content/guides/de/anythingmcp-cli.mdx

Large diffs are not rendered by default.

318 changes: 284 additions & 34 deletions content/guides/en/anythingmcp-cli.mdx

Large diffs are not rendered by default.

320 changes: 285 additions & 35 deletions content/guides/it/anythingmcp-cli.mdx

Large diffs are not rendered by default.

115 changes: 109 additions & 6 deletions packages/backend/src/adapters/adapters.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Query,
Res,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiSecurity, ApiBody, ApiResponse } from '@nestjs/swagger';
import { SkipThrottle } from '@nestjs/throttler';
import type { Response } from 'express';
import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard';
Expand Down Expand Up @@ -55,9 +55,41 @@ export class AdaptersPublicController {
description:
'Returns metadata for all pre-configured adapters. This endpoint is public so the marketing website can render the marketplace dynamically. ' +
'With `?fields=slim` each adapter carries only what a catalog page shows (no setup instructions, variable help or prerequisites), ' +
'plus an `enriched` flag; the instructions of one adapter are at `/api/adapters/:slug/instructions`.',
'plus an `enriched` flag; the instructions of one adapter are at `/api/adapters/:slug/instructions`.\n\n' +
'There is no server-side search: fetch the slim list once (it is cached with an ETag) and filter on `slug`, `name`, ' +
'`category`, `region` or `description`. Then read one adapter with `GET /api/adapters/{slug}` to see the variables ' +
'it needs (`requiredEnvVars`, `envVars`) before installing it with `POST /api/adapters/{slug}/import`.\n\n' +
'```bash\ncurl -s "$AMCP_URL/api/adapters?fields=slim" | jq \'.[] | select(.category == "crm") | .slug\'\n```',
})
@ApiQuery({
name: 'fields',
required: false,
enum: ['slim', 'full'],
description: '`slim` for the catalog-page view; omit (or `full`) for every field.',
example: 'slim',
})
@ApiResponse({
status: 200,
description: 'An array of adapters (slim view shown).',
schema: {
type: 'array',
items: { type: 'object' },
example: [
{
slug: 'hubspot',
name: 'HubSpot',
description: 'Let your AI work in HubSpot CRM: search and update contacts, companies, deals and tickets…',
region: 'intl',
category: 'crm',
icon: 'hubspot',
docsUrl: 'https://developers.hubspot.com/docs/api-reference/latest/overview',
requiredEnvVars: ['HUBSPOT_ACCESS_TOKEN'],
toolCount: 14,
enriched: true,
},
],
},
})
@ApiQuery({ name: 'fields', required: false, enum: ['slim'] })
list(
@Query('fields') fields: string | undefined,
@Headers('if-none-match') ifNoneMatch: string | undefined,
Expand Down Expand Up @@ -118,6 +150,7 @@ export class AdaptersPublicController {
// Authenticated endpoints
@ApiTags('Adapters')
@ApiBearerAuth()
@ApiSecurity('api-key')
@UseGuards(JwtOrManagementKeyGuard)
@Controller('api/adapters')
export class AdaptersController {
Expand Down Expand Up @@ -258,7 +291,11 @@ export class AdaptersController {
@ApiOperation({
summary: 'Get adapter details including tool definitions',
description:
'Returns the full adapter definition with connector config and all tool mappings.',
'Returns the full adapter definition with connector config and all tool mappings, plus what installing it takes: ' +
'`requiredEnvVars` (the variables to pass as `credentials` on import), `envVars` (each variable described: label, ' +
'kind, whether it is secret, help text), `setupKind` and `instructions` (Markdown setup guide). ' +
'404 when the slug is unknown or the adapter is not offered on this deployment.\n\n' +
'```bash\namcp api GET /api/adapters/hubspot\n```',
})
getBySlug(@Param('slug') slug: string) {
return this.adaptersService.describe(slug);
Expand All @@ -270,7 +307,34 @@ export class AdaptersController {
summary: 'Try an adapter with credentials before saving it',
description:
'Runs the adapter\'s safe read call against an in-memory connector. Nothing is stored. ' +
'ok=true: it worked; ok=false: a value is missing or the API refused; ok=null: nothing to try yet (needs a sign-in at the provider, or no safe call).',
'ok=true: it worked; ok=false: a value is missing or the API refused; ok=null: nothing to try yet (needs a sign-in at the provider, or no safe call).\n\n' +
'Pass `connectorId` to test new values for an existing connector of this adapter. Each try is a real call to the ' +
'provider: at most 20 tries a minute per user (429 beyond). VIEWERs get 403.',
})
@ApiBody({
required: false,
schema: {
type: 'object',
properties: {
credentials: {
type: 'object',
additionalProperties: { type: 'string' },
description: 'Values for the adapter variables (see `requiredEnvVars` on GET /api/adapters/{slug}).',
},
connectorId: { type: 'string', description: 'An existing connector of this adapter to test the values for.' },
},
},
examples: {
hubspot: { summary: 'Check a HubSpot key', value: { credentials: { HUBSPOT_ACCESS_TOKEN: 'pat-eu1-…' } } },
},
})
@ApiResponse({
status: 200,
description: 'The outcome of the test call.',
schema: {
type: 'object',
example: { ok: true, toolName: 'hubspot_list_owners', durationMs: 412, sample: '{"results":[{"id":"…","email":"…"}],…}' },
},
})
async verify(
@Req() req: any,
Expand Down Expand Up @@ -334,7 +398,46 @@ export class AdaptersController {
summary: 'Import a built-in adapter as a new connector',
description:
'Creates a new connector and its tools from a pre-configured adapter recipe. ' +
'Optionally provide credentials in the request body to make the connector immediately functional.',
'Optionally provide credentials in the request body to make the connector immediately functional.\n\n' +
'The keys of `credentials` are the adapter\'s variable names (`requiredEnvVars` on GET /api/adapters/{slug}). ' +
'The new connector is added straight away to your default MCP server, the oldest active one you own (`attachedToServer`, null when you have none), ' +
'and `probe` reports the first test call made with the values given (null when none was made). ' +
'403 for VIEWERs or when the plan allows no more connectors.\n\n' +
'```bash\namcp api POST /api/adapters/hubspot/import --data \'{"credentials":{"HUBSPOT_ACCESS_TOKEN":"pat-eu1-…"}}\' --yes\n```',
})
@ApiBody({
required: false,
schema: {
type: 'object',
properties: {
credentials: {
type: 'object',
additionalProperties: { type: 'string' },
description: 'Values for the adapter variables, by name. Omit to install now and fill them in later.',
},
},
},
examples: {
hubspot: {
summary: 'Install HubSpot with its key',
value: { credentials: { HUBSPOT_ACCESS_TOKEN: 'pat-eu1-…' } },
},
keyless: { summary: 'Install an adapter that needs no values', value: {} },
},
})
@ApiResponse({
status: 201,
description: 'The connector was created.',
schema: {
type: 'object',
example: {
message: 'Adapter "hubspot" imported successfully with 14 tools.',
connectorId: 'cmob60dvv00c01zrpeujwmnep',
toolsCreated: 14,
attachedToServer: { id: 'cmo9x1k2a0001abcdpq7rs8tu', name: 'Default' },
probe: { ok: true, toolName: 'hubspot_list_owners', durationMs: 388, sample: '{"results":[{"id":"…","email":"…"}],…}' },
},
},
})
async importAdapter(
@Req() req: any,
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/adapters/setup-links.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
UseGuards,
} from '@nestjs/common';
import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiOperation, ApiTags, ApiSecurity } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import type { Request } from 'express';
import { ConnectorSetupService } from './connector-setup.service';
Expand All @@ -37,6 +37,7 @@ export class SetupLinksController {
@Post('resolve')
@HttpCode(200)
@ApiBearerAuth()
@ApiSecurity('api-key')
@UseGuards(JwtOrManagementKeyGuard)
@Throttle({ default: { limit: 20, ttl: 60_000 } })
@ApiOperation({ summary: 'Open a connector setup link with the dashboard session' })
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/alerts/alerts.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Post, Put, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags, ApiSecurity } from '@nestjs/swagger';
import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard';
import { Throttle } from '@nestjs/throttler';
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUrl, Max, MaxLength, Min } from 'class-validator';
Expand Down Expand Up @@ -54,6 +54,7 @@ class SaveAlertWebhookDto {

@ApiTags('Alerts')
@ApiBearerAuth()
@ApiSecurity('api-key')
@UseGuards(JwtOrManagementKeyGuard, RolesGuard)
@Roles('ADMIN')
@Controller('api/admin/settings')
Expand Down
100 changes: 87 additions & 13 deletions packages/backend/src/audit/audit.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery, ApiSecurity, ApiResponse } from '@nestjs/swagger';
import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard';
import { AuditService } from './audit.service';
import {
Expand All @@ -11,20 +11,65 @@ import { ToolPerformanceQueryDto } from './tool-performance-query.dto';

@ApiTags('Audit')
@ApiBearerAuth()
@ApiSecurity('api-key')
@UseGuards(JwtOrManagementKeyGuard)
@Controller('api/audit')
export class AuditController {
constructor(private readonly auditService: AuditService) {}

@Get('invocations')
@ApiOperation({ summary: 'List tool invocation logs' })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'offset', required: false, type: Number })
@ApiQuery({ name: 'toolId', required: false, type: String })
@ApiQuery({ name: 'status', required: false, enum: ['SUCCESS', 'ERROR', 'TIMEOUT'] })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'connectorId', required: false, type: String })
@ApiQuery({ name: 'mcpServerId', required: false, type: String })
@ApiOperation({
summary: 'List tool invocation logs',
description:
'The audit log of tool calls in your workspace, newest first: tool and connector, caller, MCP server, `input`, ' +
'`output` or `error`, `status`, `durationMs` and `clientInfo` (how the caller authenticated, which key or client). ' +
'A failure repeated within a minute is stored once with `repeatCount`. Filters combine with AND.\n\n' +
'Typical uses: the last failures (`status=ERROR`), the calls of one tool or connector, or what one AI client did ' +
'on one server (`mcpServerId`).\n\n' +
'```bash\namcp api GET "/api/audit/invocations?status=ERROR&limit=20"\n```',
})
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Rows to return (default 100).', example: 20 })
@ApiQuery({ name: 'offset', required: false, type: Number, description: 'Rows to skip, for paging.', example: 0 })
@ApiQuery({ name: 'toolId', required: false, type: String, description: 'Only calls of this tool.' })
@ApiQuery({
name: 'status',
required: false,
enum: ['SUCCESS', 'ERROR', 'TIMEOUT'],
description: 'Only calls with this outcome.',
example: 'ERROR',
})
@ApiQuery({
name: 'search',
required: false,
type: String,
description: 'Tool name contains this text (case-insensitive).',
example: 'hubspot_',
})
@ApiQuery({ name: 'connectorId', required: false, type: String, description: 'Only calls of tools of this connector.' })
@ApiQuery({ name: 'mcpServerId', required: false, type: String, description: 'Only calls made through this MCP server.' })
@ApiResponse({
status: 200,
schema: {
type: 'array',
items: { type: 'object' },
example: [
{
id: 'cmod9e8f700f01zrpa1b2c3d4',
toolId: 'cmob7a1b200d01zrp5h6j7k8l',
status: 'ERROR',
durationMs: 742,
error: 'HTTP 403: missing scope crm.objects.deals.read',
input: { objectType: 'deals', limit: 10 },
output: null,
repeatCount: 1,
createdAt: '2026-11-05T09:41:12.000Z',
tool: { name: 'hubspot_list_objects', connectorId: 'cmob60dvv00c01zrpeujwmnep', connector: { name: 'HubSpot', type: 'REST' } },
user: { id: 'cmo8a7b6c0000abcdx1y2z3w4', email: 'ana@example.com', name: 'Ana' },
mcpServer: { id: 'cmo9x1k2a0001abcdpq7rs8tu', name: 'Sales', slug: 'sales' },
},
],
},
})
async listInvocations(
@Req() req: any,
@Query('limit') limit?: string,
Expand All @@ -43,7 +88,17 @@ export class AuditController {
}

@Get('stats')
@ApiOperation({ summary: 'Get invocation statistics' })
@ApiOperation({
summary: 'Get invocation statistics',
description: 'Tool-call counters for your workspace: last 24 hours, errors in the last 24 hours, last 7 days and all time.',
})
@ApiResponse({
status: 200,
schema: {
type: 'object',
example: { invocations24h: 312, errors24h: 7, invocations7d: 2140, totalInvocations: 48211 },
},
})
async getStats(@Req() req: any) {
return this.auditService.getStats(req.user.organizationId);
}
Expand All @@ -53,7 +108,8 @@ export class AuditController {
summary: 'Get analytics data with daily time-series and top tools',
description:
'Returns a daily breakdown (UTC days, last `days`, default 7) of invocations by status, ' +
'top 10 most-used tools, success rate, and average duration.',
'top 10 most-used tools, success rate, and average duration.\n\n' +
'```bash\namcp api GET "/api/audit/analytics?days=30"\n```',
})
@ApiQuery({
name: 'days',
Expand Down Expand Up @@ -94,9 +150,27 @@ export class AuditController {
description:
'Aggregates the last N days (default 30) of tool calls per connector, per ' +
'MCP server and per user, with error counts, proxy-call metering and a ' +
'volume-based cost estimate.',
'volume-based cost estimate. Each list holds the top 20 by calls; `estCostMicros` is 0 unless the ' +
'instance sets per-call rates.',
})
@ApiQuery({ name: 'days', required: false, type: Number, description: 'Window in days, 1 to 365 (default 30).', example: 30 })
@ApiResponse({
status: 200,
schema: {
type: 'object',
example: {
days: 30,
total: 9120,
errors: 143,
proxyCalls: 0,
estCostMicros: 0,
rates: { callMicros: 0, proxyCallMicros: 0 },
byConnector: [{ id: 'cmob60dvv00c01zrpeujwmnep', label: 'HubSpot', count: 5210, errors: 61 }],
byServer: [{ id: 'cmo9x1k2a0001abcdpq7rs8tu', label: 'Sales', count: 6001, errors: 70 }],
byUser: [{ id: 'cmo8a7b6c0000abcdx1y2z3w4', label: 'Ana', count: 3302, errors: 12 }],
},
},
})
@ApiQuery({ name: 'days', required: false, type: Number })
async getBreakdowns(@Req() req: any, @Query('days') days?: string) {
return this.auditService.getBreakdowns(
req.user.organizationId,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/auth/login.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { hasDashboardHeader, setRecentSignInCookie } from './recent-sign-in.util
import { RecentSignInService } from './recent-sign-in.service';
import { SharedSetupRegistry, type SetupContext, type SharedSetupProvider } from '../mcp-server/shared-setup';
import { assistantForRedirectUris, profileForRedirectUris } from '../mcp-server/shared-toolset';
import { ApiTags } from '@nestjs/swagger';

/**
* Carries the VERIFIED identity across the second step of the authorize flow.
Expand Down Expand Up @@ -170,6 +171,7 @@ export function fileSearchWizardPathFor(optionId: string, serverIds: readonly st
return `/connectors/new/file-search?${q.toString()}`;
}

@ApiTags('MCP client sign-in')
@Controller('auth')
export class LoginController {
private readonly logger = new Logger(LoginController.name);
Expand Down
Loading
Loading