diff --git a/content/guides/de/anythingmcp-cli.mdx b/content/guides/de/anythingmcp-cli.mdx index b8380dc8b..e29a8a882 100644 --- a/content/guides/de/anythingmcp-cli.mdx +++ b/content/guides/de/anythingmcp-cli.mdx @@ -1,83 +1,333 @@ --- title: "Die AnythingMCP CLI — Ihre MCP-Instanz vollständig im Terminal verwalten" -description: "amcp ist der offizielle AnythingMCP-Kommandozeilen-Client. Rufen Sie MCP-Tools in der Shell auf und verwalten Sie Ihre Instanz komplett — Connectors, MCP-Server, Benutzer, Rollen, SSO und Einstellungen — mit einem Management-API-Schlüssel. Per npm installieren, im Browser anmelden, alles skripten. Für Menschen und für verbundene KI-Agenten." +description: "amcp ist der offizielle AnythingMCP-Kommandozeilen-Client. Rufen Sie MCP-Tools mit echten Argumenten auf, leiten Sie Ergebnisse an jq und cron weiter, führen Sie Code-Mode-Programme aus, die Dutzende Tool-Aufrufe in einem Schritt bündeln, und skripten Sie die ganze Instanz — Connectors, MCP-Server, Rollen, Audit — mit einem Management-Schlüssel. Konkrete Beispiele mit Shopify, HubSpot und Slack." category: "announcements" -keyword: "AnythingMCP CLI, amcp CLI, MCP CLI, Model Context Protocol CLI, MCP Kommandozeile, MCP Server im Terminal verwalten, MCP Gateway CLI, npm @anythingmcp/cli, MCP Tools im Terminal, KI-Agent CLI" +keyword: "AnythingMCP CLI, amcp CLI, MCP CLI, Model Context Protocol CLI, MCP Kommandozeile, MCP Code-Mode, code_execute, MCP Server im Terminal verwalten, MCP Gateway CLI, npm @anythingmcp/cli, MCP Tools im Terminal, KI-Agent CLI" publishedAt: "2026-11-04" +updatedAt: "2026-11-05" lang: "de" featured: true --- ## Ihre ganze MCP-Instanz, aus dem Terminal -Das ist **`amcp`**, der offizielle AnythingMCP-Kommandozeilen-Client. Er kann zweierlei: +**`amcp`** ist der offizielle AnythingMCP-Kommandozeilen-Client. Ein Binary, zwei Aufgaben: -1. **MCP-Tools aufrufen** aus Ihrer Shell — entdecken, ihr Schema lesen, ausführen. -2. **Die Management-API steuern** (`/api/*`) mit einem **Management-API-Schlüssel** — damit ein Mensch oder ein verbundener KI-Agent die gesamte Instanz konfigurieren kann (Connectors, MCP-Server, Benutzer, Rollen, SSO, Memory, Skills, den Knowledge Graph und Einstellungen), ohne das Terminal zu verlassen. +1. **MCP-Tools aufrufen** — dieselben Tools, die Ihre KI-Clients nutzen (Shopify, HubSpot, SAP, Ihre SQL-Datenbank, ein Dateishare…). Von Hand, in Shell-Skripten, per cron und CI oder als **Code-Mode-Programm**, das Dutzende Aufrufe in einem Schritt verkettet. +2. **Die Management-API steuern** (`/api/*`) mit einem **Management-Schlüssel**. Alles, was das Dashboard kann — Connectors, MCP-Server, Benutzer, Rollen, SSO, Memory, Skills, Audit — wird zum Befehl, für Sie oder einen verbundenen Agenten. -Er ist Open Source (AGPL-3.0), funktioniert mit **AnythingMCP Cloud** und **selbst gehosteten** Instanzen und wird per npm installiert: +Die CLI ist Open Source (AGPL-3.0), funktioniert mit **AnythingMCP Cloud** und **selbst gehosteten** Instanzen, braucht Node 22+ und wird über npm installiert: ```bash npm i -g @anythingmcp/cli amcp --help ``` -## Anmelden +Alle Beispiele unten verwenden echte Befehle, Flags, Endpunkte und Tool-Namen. Die Shopify-, HubSpot- und Slack-Tools stammen aus den Katalog-Connectors. Nur die Daten in der Ausgabe sind erfunden. -Erstellen Sie einen **Management-API-Schlüssel** im Dashboard (Einstellungen → Management-API-Schlüssel, nur Lesen oder Admin), oder lassen Sie es die CLI über Ihren Browser erledigen: +## Anmelden: zwei Arten von Schlüsseln + +`amcp` arbeitet mit zwei Arten von Schlüsseln und hält sie in getrennten Profilen: + +| Sie möchten… | Schlüssel | Anmelden mit | +|---|---|---| +| Die Tools eines MCP-Servers aufrufen | einen **MCP-Schlüssel** für diesen Server | `amcp login --url … --server SERVER_ID` | +| Die Instanz verwalten (`amcp api`) | einen **Management-Schlüssel** (nur lesend oder Admin) | `amcp login --browser` | + +Für einen Management-Schlüssel übernimmt der Browser die Arbeit: ```bash -amcp login --browser --url https://cloud.anythingmcp.com # nur Lesen -amcp login --browser --scope admin --url https://cloud.anythingmcp.com +amcp login --browser --url https://cloud.anythingmcp.com # read-only +amcp login --browser --scope admin --url https://cloud.anythingmcp.com # read + write ``` -`--browser` öffnet die Autorisierungsseite der Instanz, Sie bestätigen unter Ihrer bestehenden Dashboard-Sitzung (inkl. SSO), und der Schlüssel kommt über einen einmaligen `127.0.0.1`-Loopback zur CLI zurück — er taucht nie in einer URL, einem Log oder der Shell-Historie auf. Um sich stattdessen mit einem MCP-Server für Tool-Aufrufe zu verbinden, nutzen Sie `amcp login --url … --server …` mit einem MCP-Schlüssel. +Die CLI öffnet die Autorisierungsseite der Instanz und zeigt einen kurzen Code an. Sie prüfen, dass er mit der Seite übereinstimmt, und bestätigen mit Ihrer normalen Dashboard-Sitzung (inklusive SSO). Der Schlüssel kommt über einen einmaligen `127.0.0.1`-Loopback zurück und taucht nie in einer URL, einem Log oder der Shell-Historie auf. -## MCP-Tools aufrufen +Für Tool-Aufrufe fügen Sie auf Nachfrage einen MCP-Schlüssel ein. Die CLI zeigt ihn nicht an und prüft ihn vor dem Speichern mit einem echten `tools/list`: ```bash -amcp tools ls # Tools auflisten, mit Lese-/Schreib-Hinweisen -amcp tools describe YOUR_TOOL # volle Beschreibung + Eingabe-JSON-Schema -amcp tools call YOUR_TOOL --args '{"id":"123"}' +amcp login --url https://cloud.anythingmcp.com --server cmob60dvv00c01zrpeujwmnep --profile shop +amcp whoami --profile shop +``` + +## 1. Tools erkunden und aufrufen + +Fragen Sie den Server zuerst, was er kann. `tools ls` zeigt für jedes Tool den Namen, den Hinweis nur lesend/destruktiv und die erste Zeile der Beschreibung: + +```console +$ amcp tools ls --filter order +shopify_search_orders read-only Search orders with Shopify search syntax (for example `financial_status:paid … +shopify_get_order read-only Get one order: status, totals (subtotal, shipping, tax, discounts, refunds) … +``` + +`tools describe` zeigt die vollständige Beschreibung und das JSON-Eingabeschema, damit Sie genau wissen, welche Argumente ein Tool erwartet. Dann rufen Sie es mit echten Argumenten auf: + +```console +$ amcp tools call shopify_search_orders \ + --args '{"query":"financial_status:paid created_at:>=2026-11-01","first":2}' +{ + "orders": { + "pageInfo": { "hasNextPage": true, "endCursor": "eyJsYXN0X2lkIjo2…" }, + "nodes": [ + { + "name": "#1043", + "createdAt": "2026-11-04T16:12:09Z", + "displayFinancialStatus": "PAID", + "displayFulfillmentStatus": "UNFULFILLED", + "currentTotalPriceSet": { "shopMoney": { "amount": "412.80", "currencyCode": "EUR" } }, + "customer": { "displayName": "Kellerhaus GmbH" } + }, + … +``` + +Argumente können auch aus einer Datei (`--args @args.json`) oder von stdin (`--args -`) kommen. Ein als destruktiv markiertes Tool fragt im Terminal nach einer Bestätigung. In einem Skript müssen Sie `--yes` ausdrücklich angeben. + +## 2. In Ihre Skripte einbauen + +Die Textausgabe eines Tools ist sein JSON auf stdout. Fehler gehen nach stderr, mit eigenem Exit-Code. Damit verhält sich `amcp` wie jedes andere Unix-Werkzeug: + +```console +$ amcp tools call shopify_search_orders \ + --args '{"query":"fulfillment_status:unfulfilled created_at:<2026-11-03"}' \ + | jq -r '.orders.nodes[] | [.name, .customer.displayName, .createdAt[:10]] | @tsv' +#1038 Kellerhaus GmbH 2026-10-30 +#1035 Studio Lumen 2026-10-29 +#1031 Bäckerei Hofmann 2026-10-28 ``` -## Die Instanz verwalten +Exit-Codes: `0` Erfolg, `1` das Tool hat einen Fehler gemeldet, `2` ungültige Verwendung oder ungültiges JSON, `3` Authentifizierung, `4` Verbindung, Serverfehler oder Timeout. -Alles, was das Dashboard kann, kann `amcp api` mit einem Management-Schlüssel — es ist `gh api` für AnythingMCP: +Kombiniert ergibt das einen täglichen Job. Dieser postet jeden Morgen verspätete Bestellungen in einen Slack-Kanal (der Slack-Connector liegt auf demselben Server): ```bash -amcp api GET /api/connectors -amcp api GET /api/mcp-servers -amcp api POST /api/mcp-servers --data '{"name":"ops","description":"Ops-Server"}' -amcp api PUT /api/mcp-servers/SERVER_ID --data '{"codeMode":"tools"}' +#!/usr/bin/env bash +# late-orders.sh — run from cron: 0 8 * * 1-5 +set -euo pipefail +SINCE=$(date -d '-2 days' +%F) # GNU date; on macOS: date -v-2d +%F +LATE=$(amcp tools call shopify_search_orders \ + --args "{\"query\":\"fulfillment_status:unfulfilled created_at:<$SINCE\"}" \ + | jq -r '.orders.nodes[] | "• \(.name) — \(.customer.displayName)"') +[ -z "$LATE" ] && exit 0 +jq -n --arg text "Orders waiting more than 2 days: +$LATE" '{channel: "#fulfilment", text: $text}' \ + | amcp tools call slack_post_message --args - --yes ``` -Nicht sicher, was verfügbar ist? Die CLI bringt einen Katalog aller Management-Operationen mit — ohne Schlüssel, ohne Netzwerk: +In der CI brauchen Sie kein Profil: `AMCP_URL`, `AMCP_SERVER` und `AMCP_API_KEY` liefern die Verbindung, und mit `--json` gibt jeder Befehl genau ein JSON-Objekt aus. + +## 3. Code-Mode: ein Programm, viele Tool-Aufrufe + +Skripte wie das obige machen einen Aufruf nach dem anderen von Ihrem Rechner aus. Der **Code-Mode** schickt stattdessen ein ganzes Programm an den Server. Steht der Code-Mode eines MCP-Servers auf `inline`, bietet der Server ein Tool namens `code_execute` an. Sie senden ihm ein kurzes JavaScript-Programm, und eine Sandbox neben Ihren Tools führt es aus, mit drei Hilfsfunktionen: + +- `await amcp.search("query")` findet Tools nach Name oder Beschreibung; +- `await amcp.describe("tool")` liefert das Eingabeschema eines Tools; +- `await amcp.run("tool", args)` ruft ein Tool auf und gibt das Ergebnis als JavaScript-Wert zurück. + +Einmal pro Server einschalten (Admin-Management-Schlüssel nötig): ```bash -amcp api --list # alle Operationen, nach Ressource gruppiert -amcp api --list mcp-servers # nur eine Ressource +amcp api PUT /api/mcp-servers/$SRV --yes --data '{"codeMode":"inline"}' +``` + +Jetzt eine Frage, die Dutzende Hin- und Rückwege bräuchte: *Wer sind unsere fünf größten Shopify-Kunden seit Oktober, und wo steht jeder davon in der HubSpot-Pipeline?* Als Programm: + +```js +// top-customers.js — runs inside the AnythingMCP sandbox: read-only, your own permissions +const byEmail = new Map(); +let after = null, pages = 0; + +// 1. Page through every paid Shopify order since October +do { + const r = await amcp.run("shopify_search_orders", { + query: "financial_status:paid created_at:>=2026-10-01", + first: 250, after, + }); + for (const o of r.orders.nodes) { + if (!o.email) continue; + const c = byEmail.get(o.email) + ?? { name: o.customer?.displayName, orders: 0, revenue: 0 }; + c.orders += 1; + c.revenue += Number(o.currentTotalPriceSet.shopMoney.amount); + byEmail.set(o.email, c); + } + const { hasNextPage, endCursor } = r.orders.pageInfo; + after = hasNextPage ? endCursor : null; + pages++; +} while (after); + +// 2. Keep the five biggest customers +const top = [...byEmail] + .sort((a, b) => b[1].revenue - a[1].revenue) + .slice(0, 5); + +// 3. Join each one with its HubSpot contact +const result = []; +for (const [email, c] of top) { + const h = await amcp.run("hubspot_search_objects", { + object_type: "contacts", + filter_groups: [{ filters: [ + { propertyName: "email", operator: "EQ", value: email }, + ] }], + properties: ["lifecyclestage"], + limit: 1, + }); + const stage = h.results[0]?.properties.lifecyclestage ?? "not in CRM"; + result.push({ email, ...c, revenue: c.revenue.toFixed(2), stage }); +} +return { pagesScanned: pages, top: result }; +``` + +`code_execute` erwartet `{"code": ""}`. `jq -Rs` verpackt die Datei für Sie: + +```console +$ jq -Rs '{code: .}' top-customers.js | amcp tools call code_execute --args - +{ + "pagesScanned": 3, + "top": [ + { + "email": "einkauf@kellerhaus.de", + "name": "Kellerhaus GmbH", + "orders": 14, + "revenue": "4820.50", + "stage": "customer" + }, + { + "email": "m.weber@example.com", + "name": "Martin Weber", + "orders": 6, + "revenue": "2475.90", + "stage": "not in CRM" + }, + … + ] +} +``` + +Das sind **8 Tool-Aufrufe** (3 Seiten Bestellungen und 5 CRM-Abfragen) für **einen** einzigen Aufruf aus Ihrem Terminal. Zurück kommen die fünf Kunden, nach denen Sie gefragt haben, keine 750 rohen Bestellungen. Dasselbe gilt, wenn statt Ihnen ein KI-Agent aufruft: Er liest eine kurze Antwort statt 750 Bestellungen, spart Tokens und macht weniger Fehler. + +Sie kennen die Tool-Namen noch nicht? Lassen Sie das Programm sie finden: + +```js +// discover.js +const tools = await amcp.search("invoice"); // [{ tool, description, readOnly }, …] +const schema = await amcp.describe(tools[0].tool); // { name, description, inputSchema, readOnly } +return { tools: tools.map(t => t.tool), firstSchema: schema.inputSchema }; ``` -Ein Management-Schlüssel trägt die Identität und Rolle des Besitzers: er verleiht nie zusätzliche Rechte, Admin-Routen brauchen also weiterhin einen Admin-Besitzer, und Nur-Lese-Schlüssel können nur lesen. Die API wendet ein Rate-Limit pro Schlüssel an, sodass ein außer Kontrolle geratener Agent abgewiesen wird, statt Ihre Instanz zu überlasten. +**Die Schutzmechanismen sind Teil des Designs:** -## Auch für Agenten gebaut +- **Nur lesend.** Ein Programm darf nur Tools aufrufen, die keine Daten ändern, und nur mit den Rechten des Schlüssels, der es ausführt. Schreibzugriffe bleiben ausdrückliche direkte Aufrufe, wie `slack_post_message --yes` oben. +- **Isoliert.** Das Programm läuft in einem separaten Sandbox-Prozess ohne Dateisystem, Netzwerk oder Umgebungsvariablen, mit Zeitlimit und begrenzter Zahl von Tool-Aufrufen pro Lauf. +- **Auditiert.** Jeder innere Tool-Aufruf wird wie ein direkter protokolliert, zusammen mit dem Code-Lauf, der ihn ausgelöst hat. +- **Opt-in.** Inline-Code ist aus, solange der `codeMode` des Servers nicht `inline` ist *und* Ihre Rolle die Inline-Code-Berechtigung hat. Verfügbar ab dem Team-Plan. -Dieselbe Management-API ist genau das, was ein verbundener KI-Agent braucht, um einen **Workspace von Anfang bis Ende einzurichten und zu betreiben** — einen Connector hinzufügen, einen MCP-Server erstellen, Rollen zuweisen, SSO einrichten, Memory und Skills umschalten — sicher, innerhalb des Umfangs, den Sie ihm gewähren. Agenten sind standardmäßig nur lesend. +### Die guten behalten: als Tool vorschlagen -## Skripte und CI +Wenn ein Programm eine Frage beantwortet, die immer wieder kommt, speichert `code_propose_tool` es als Vorschlag für einen Admin. Ausgeführt wird dabei nichts. Sobald ein Admin den Vorschlag freigibt, bekommt jeder KI-Client auf diesem Server ihn als normales, benanntes Tool: + +```console +$ jq -Rs '{ + name: "top_customers_with_crm_stage", + description: "Top 5 Shopify customers since October, with their HubSpot lifecycle stage", + rationale: "Sales asks for this every Monday", + code: . + }' top-customers.js | amcp tools call code_propose_tool --args - +{"ok":true,"proposalId":"cmh4k2…","message":"Proposal recorded for admin review. It will not run or become a tool until an admin approves it."} +``` + +Mehr dazu in [Code-Mode für KI-Agenten](/de/guides/code-mode). + +## 4. Die Instanz verwalten + +Mit einem Management-Schlüssel ruft `amcp api METHODE PFAD` jede `/api/*`-Route auf. Es funktioniert wie `gh api`, nur für AnythingMCP. Die Textausgabe ist der geparste JSON-Body, also funktioniert auch hier `jq`. + +### Einen Workspace in einem Skript aufsetzen + +Einen Katalog-Connector mit seinem Token installieren, dem Vertrieb einen eigenen MCP-Server geben, den Connector zuweisen, Code-Mode einschalten und die Verbindung prüfen: ```bash -AMCP_URL=https://cloud.anythingmcp.com AMCP_API_KEY=mcp_… amcp api GET /api/users --json -amcp completion zsh > "${fpath[1]}/_amcp" # Shell-Vervollständigung für bash, zsh, fish +#!/usr/bin/env bash +# bootstrap.sh — needs: amcp login --browser --scope admin +set -euo pipefail + +CONN=$(jq -n --arg t "$HUBSPOT_TOKEN" '{credentials: {HUBSPOT_ACCESS_TOKEN: $t}}' \ + | amcp api POST /api/adapters/hubspot/import --data - --yes | jq -r .connectorId) + +SRV=$(amcp api POST /api/mcp-servers --yes \ + --data '{"name":"Sales","description":"CRM tools for the sales team"}' | jq -r .id) + +amcp api PUT /api/mcp-servers/$SRV/connectors --yes --data "{\"connectorIds\":[\"$CONN\"]}" +amcp api PUT /api/mcp-servers/$SRV --yes --data '{"codeMode":"inline"}' +amcp api POST /api/connectors/$CONN/test --yes +# → { "ok": true, "kind": "ok", "message": "Connection successful" } ``` -Schlüssel werden mit Modus `0600` gespeichert, HTTPS ist Pflicht (außer Loopback für lokale Entwicklung), Weiterleitungen werden abgelehnt, damit ein Schlüssel keiner Anfrage zu einem anderen Origin folgt, und der Schlüssel wird nie als Kommandozeilen-Argument akzeptiert. +Das Token erreicht die API über `jq --arg` und stdin und erscheint daher nie in `ps` oder der Shell-Historie. In einem Skript braucht jeder Aufruf außer GET ein `--yes`. Das ist Absicht: Ein Skript kann nichts versehentlich ändern. + +### Die eigene API zu Tools machen + +Nicht im Katalog? Legen Sie einen REST-Connector an und importieren Sie seine OpenAPI-Spezifikation. WSDL, GraphQL, Postman, cURL und HAR funktionieren genauso, über das Feld `source`: + +```bash +ID=$(amcp api POST /api/connectors --yes --data '{ + "name": "Acme Billing", "type": "REST", + "baseUrl": "https://api.acme.example/v1", + "authType": "BEARER_TOKEN", "authConfig": { "token": "'"$ACME_TOKEN"'" } +}' | jq -r .id) + +amcp api POST /api/connectors/$ID/import --yes \ + --data '{"source":"openapi","url":"https://api.acme.example/v1/openapi.json"}' +``` + +### Sehen, was Ihre Agenten getan haben + +Jeder Tool-Aufruf, ob von Menschen oder Agenten, landet im Audit-Log: + +```console +$ amcp api GET "/api/audit/invocations?status=ERROR&limit=3" \ + | jq -r '.[] | [.createdAt[11:16], .tool.name, .user.email, .error[:40]] | @tsv' +08:41 hubspot_search_objects lea@acme.io HTTP 429: retry after 1s +07:02 shopify_get_order ops-agent@acme.io Order not found +06:55 zendesk_get_ticket marco@acme.io HTTP 404: RecordNotFound +``` + +`GET /api/audit/breakdowns?days=30` liefert Nutzung und Kosten nach Connector, MCP-Server und Benutzer aufgeschlüsselt. Das passt gut für einen Monatsbericht. + +### Die ganze API offline entdecken + +Sie brauchen die Doku nicht geöffnet zu haben. Der Katalog der Management-Operationen wird mit der CLI ausgeliefert: + +```console +$ amcp api --list mcp-servers +mcp-servers + GET /api/mcp-servers — List MCP servers for current organization + POST /api/mcp-servers [body] — Create a new MCP server + DELETE /api/mcp-servers/:id — Delete MCP server + GET /api/mcp-servers/:id — Get MCP server detail + PUT /api/mcp-servers/:id [body] — Update MCP server + … +``` + +`[admin]` markiert Routen, die einen Admin als Besitzer brauchen, `[body]` Routen mit Request-Body. Der Katalog wird aus den Backend-Controllern neu erzeugt, und die CI schlägt fehl, wenn eine Route fehlt. Er passt also immer zu Ihrer Instanz. Die interaktive Referenz mit Request-Beispielen finden Sie unter [/developers/api](/de/developers/api). + +## 5. Einem Agenten übergeben + +Ein Coding-Agent wie Claude Code, Codex oder der Agent von Cursor kann `amcp` genauso bedienen wie Sie. Mit einem **nur lesenden** Management-Schlüssel prüft er einen Workspace: welche Server welche Connectors anbieten und welche Aufrufe fehlschlagen. Mit einem **Admin**-Schlüssel richtet er mit den Befehlen aus Abschnitt 4 ein neues Team komplett ein. + +Der Umfang des Schlüssels ist das Sicherheitsnetz. Ein Management-Schlüssel trägt Identität und Rolle seines Besitzers und verleiht nie mehr. Ein nur lesender Schlüssel kann nur `GET`. Jeder Aufruf ist pro Schlüssel rate-limitiert (HTTP 429 statt einer überlasteten Instanz) und wird auditiert. + +## Sicher von Haus aus + +- Der Schlüssel wird nie als Befehlsargument akzeptiert und nie ausgegeben. `whoami` zeigt nur ein Präfix. +- Profile liegen in `~/.config/amcp/config.json` mit Dateimodus `0600`. +- HTTPS ist Pflicht, außer Loopback für die lokale Entwicklung. +- Weiterleitungen werden abgelehnt, damit ein Schlüssel keiner Anfrage zu einem anderen Origin folgt. ## Loslegen -- **Installieren:** `npm i -g @anythingmcp/cli` -- **Doku:** die vollständige Befehlsreferenz steht im [CLI-README auf GitHub](https://github.com/HelpCode-ai/anythingmcp/tree/main/packages/cli#readme). -- **Quellcode:** [github.com/HelpCode-ai/anythingmcp](https://github.com/HelpCode-ai/anythingmcp). +- **Installieren:** `npm i -g @anythingmcp/cli`, dazu Shell-Vervollständigung mit `amcp completion zsh` (auch bash und fish) +- **Überblick:** [anythingmcp.com/developers/cli](/de/developers/cli) +- **Vollständige Befehlsreferenz:** das [CLI-README auf GitHub](https://github.com/HelpCode-ai/anythingmcp/tree/main/packages/cli#readme) +- **Quellcode:** [github.com/HelpCode-ai/anythingmcp](https://github.com/HelpCode-ai/anythingmcp) -Noch kein AnythingMCP? Kostenlos starten auf [cloud.anythingmcp.com](https://cloud.anythingmcp.com), oder selbst hosten. +Noch kein AnythingMCP? Starten Sie kostenlos auf [cloud.anythingmcp.com](https://cloud.anythingmcp.com) oder hosten Sie es selbst. diff --git a/content/guides/en/anythingmcp-cli.mdx b/content/guides/en/anythingmcp-cli.mdx index 0073ebc35..bfa7c528e 100644 --- a/content/guides/en/anythingmcp-cli.mdx +++ b/content/guides/en/anythingmcp-cli.mdx @@ -1,83 +1,333 @@ --- title: "The AnythingMCP CLI — Run and Manage Your MCP Instance From the Terminal" -description: "amcp is the official AnythingMCP command-line client. Call MCP tools from your shell and fully manage your instance — connectors, MCP servers, users, roles, SSO and settings — with a management-scoped API key. Install with npm, log in through the browser, script everything. Works for people and for connected AI agents." +description: "amcp is the official AnythingMCP command-line client. Call MCP tools with real arguments, pipe results into jq and cron, run code-mode programs that loop over dozens of tool calls in one step, and script your whole instance — connectors, MCP servers, roles, audit — with a management key. Worked examples with Shopify, HubSpot and Slack." category: "announcements" -keyword: "AnythingMCP CLI, amcp CLI, MCP CLI, Model Context Protocol CLI, MCP command line, manage MCP server terminal, MCP gateway CLI, npm @anythingmcp/cli, MCP tools from terminal, AI agent CLI" +keyword: "AnythingMCP CLI, amcp CLI, MCP CLI, Model Context Protocol CLI, MCP command line, MCP code mode, code_execute, manage MCP server terminal, MCP gateway CLI, npm @anythingmcp/cli, MCP tools from terminal, AI agent CLI" publishedAt: "2026-11-04" +updatedAt: "2026-11-05" lang: "en" featured: true --- ## Your whole MCP instance, from the terminal -Meet **`amcp`**, the official AnythingMCP command-line client. It does two things: +**`amcp`** is the official AnythingMCP command-line client. It does two things with one binary: -1. **Call MCP tools** from your shell — discover them, read their schema, run them. -2. **Drive the management API** (`/api/*`) with a **management-scoped API key** — so a person, or a connected AI agent, can configure the entire instance (connectors, MCP servers, users, roles, SSO, memory, skills, the knowledge graph and settings) without leaving the terminal. +1. **Calls MCP tools** — the same tools your AI clients use (Shopify, HubSpot, SAP, your SQL database, a file share…). You can run them by hand, in shell scripts, from cron and CI, or as a **code-mode program** that chains dozens of calls in one step. +2. **Drives the management API** (`/api/*`) with a **management-scoped key**. Everything the dashboard does — connectors, MCP servers, users, roles, SSO, memory, skills, audit — becomes a command, for you or for a connected agent. -It is open source (AGPL-3.0), works against **AnythingMCP Cloud** and **self-hosted** instances, and installs from npm: +It is open source (AGPL-3.0), works against **AnythingMCP Cloud** and **self-hosted** instances, needs Node 22+, and installs from npm: ```bash npm i -g @anythingmcp/cli amcp --help ``` -## Log in +Every example below uses real commands, flags, endpoints and tool names. The Shopify, HubSpot and Slack tools come from the built-in catalogue connectors. Only the data in the output is invented. -Mint a **management API key** in the dashboard (Settings → Management API keys, read-only or admin), or let the CLI do it through your browser: +## Log in: two kinds of key + +`amcp` uses two kinds of key, and keeps them in separate profiles: + +| You want to… | Key | Log in with | +|---|---|---| +| Call the tools of one MCP server | an **MCP key** for that server | `amcp login --url … --server SERVER_ID` | +| Manage the instance (`amcp api`) | a **management key** (read-only or admin) | `amcp login --browser` | + +For a management key, let the browser do the work: ```bash -amcp login --browser --url https://cloud.anythingmcp.com # read-only -amcp login --browser --scope admin --url https://cloud.anythingmcp.com +amcp login --browser --url https://cloud.anythingmcp.com # read-only +amcp login --browser --scope admin --url https://cloud.anythingmcp.com # read + write ``` -`--browser` opens the instance's authorization page, you approve it under your existing dashboard session (SSO included), and the key comes back to the CLI over a one-shot `127.0.0.1` loopback — it never appears in a URL, log or shell history. To connect to one MCP server for tool calls instead, use `amcp login --url … --server …` with an MCP key. +The CLI opens the instance's authorization page and prints a short code. You check it matches the page and approve under your normal dashboard session (SSO included). The key comes back over a one-shot `127.0.0.1` loopback, so it never appears in a URL, a log or your shell history. -## Call MCP tools +For tool calls, paste an MCP key when prompted. The CLI doesn't echo it, and it checks the key with a real `tools/list` before saving it: ```bash -amcp tools ls # list tools, with read-only/destructive hints -amcp tools describe YOUR_TOOL # full description + input JSON schema -amcp tools call YOUR_TOOL --args '{"id":"123"}' +amcp login --url https://cloud.anythingmcp.com --server cmob60dvv00c01zrpeujwmnep --profile shop +amcp whoami --profile shop +``` + +## 1. Explore and call tools + +Start by asking the server what it can do. `tools ls` prints each tool's name, its read-only/destructive hint and the first line of its description: + +```console +$ amcp tools ls --filter order +shopify_search_orders read-only Search orders with Shopify search syntax (for example `financial_status:paid … +shopify_get_order read-only Get one order: status, totals (subtotal, shipping, tax, discounts, refunds) … +``` + +`tools describe` shows the full description and the input JSON schema, so you know exactly which arguments a tool takes. Then call it with real arguments: + +```console +$ amcp tools call shopify_search_orders \ + --args '{"query":"financial_status:paid created_at:>=2026-11-01","first":2}' +{ + "orders": { + "pageInfo": { "hasNextPage": true, "endCursor": "eyJsYXN0X2lkIjo2…" }, + "nodes": [ + { + "name": "#1043", + "createdAt": "2026-11-04T16:12:09Z", + "displayFinancialStatus": "PAID", + "displayFulfillmentStatus": "UNFULFILLED", + "currentTotalPriceSet": { "shopMoney": { "amount": "412.80", "currencyCode": "EUR" } }, + "customer": { "displayName": "Kellerhaus GmbH" } + }, + … +``` + +Arguments can also come from a file (`--args @args.json`) or from stdin (`--args -`). A tool marked destructive asks for confirmation in a terminal. In a script, you must pass `--yes` explicitly. + +## 2. Pipe it into your scripts + +A tool's text output is its JSON on stdout. Errors go to stderr with their own exit code. That makes `amcp` a normal Unix citizen: + +```console +$ amcp tools call shopify_search_orders \ + --args '{"query":"fulfillment_status:unfulfilled created_at:<2026-11-03"}' \ + | jq -r '.orders.nodes[] | [.name, .customer.displayName, .createdAt[:10]] | @tsv' +#1038 Kellerhaus GmbH 2026-10-30 +#1035 Studio Lumen 2026-10-29 +#1031 Bäckerei Hofmann 2026-10-28 ``` -## Manage the instance +Exit codes: `0` success, `1` the tool returned an error, `2` invalid usage or JSON, `3` authentication, `4` connection, server error or timeout. -Everything the dashboard can do, `amcp api` can do with a management key — it is `gh api` for AnythingMCP: +Put the two together and you get a daily job. This one posts late orders to a Slack channel every morning (the Slack connector sits on the same server): ```bash -amcp api GET /api/connectors -amcp api GET /api/mcp-servers -amcp api POST /api/mcp-servers --data '{"name":"ops","description":"Ops server"}' -amcp api PUT /api/mcp-servers/SERVER_ID --data '{"codeMode":"tools"}' +#!/usr/bin/env bash +# late-orders.sh — run from cron: 0 8 * * 1-5 +set -euo pipefail +SINCE=$(date -d '-2 days' +%F) # GNU date; on macOS: date -v-2d +%F +LATE=$(amcp tools call shopify_search_orders \ + --args "{\"query\":\"fulfillment_status:unfulfilled created_at:<$SINCE\"}" \ + | jq -r '.orders.nodes[] | "• \(.name) — \(.customer.displayName)"') +[ -z "$LATE" ] && exit 0 +jq -n --arg text "Orders waiting more than 2 days: +$LATE" '{channel: "#fulfilment", text: $text}' \ + | amcp tools call slack_post_message --args - --yes ``` -Not sure what's available? The CLI ships a catalogue of every management operation — no key, no network: +In CI, skip the profile: `AMCP_URL`, `AMCP_SERVER` and `AMCP_API_KEY` supply the connection, and `--json` makes every command print one JSON object. + +## 3. Code mode: one program, many tool calls + +Scripts like the one above make one call at a time from your machine. **Code mode** sends a whole program to the server instead. With code mode set to `inline` on an MCP server, the server offers a tool called `code_execute`. You send it a short JavaScript program, and a sandbox next to your tools runs it, with three helpers: + +- `await amcp.search("query")` finds tools by name or description; +- `await amcp.describe("tool")` returns a tool's input schema; +- `await amcp.run("tool", args)` calls a tool and returns its result as a JavaScript value. + +Turn it on once per server (needs an admin management key): ```bash -amcp api --list # every operation, grouped by resource -amcp api --list mcp-servers # just one resource +amcp api PUT /api/mcp-servers/$SRV --yes --data '{"codeMode":"inline"}' +``` + +Now a question that would take dozens of round trips: *who are our five biggest Shopify customers since October, and where is each one in the HubSpot pipeline?* Write it as a program: + +```js +// top-customers.js — runs inside the AnythingMCP sandbox: read-only, your own permissions +const byEmail = new Map(); +let after = null, pages = 0; + +// 1. Page through every paid Shopify order since October +do { + const r = await amcp.run("shopify_search_orders", { + query: "financial_status:paid created_at:>=2026-10-01", + first: 250, after, + }); + for (const o of r.orders.nodes) { + if (!o.email) continue; + const c = byEmail.get(o.email) + ?? { name: o.customer?.displayName, orders: 0, revenue: 0 }; + c.orders += 1; + c.revenue += Number(o.currentTotalPriceSet.shopMoney.amount); + byEmail.set(o.email, c); + } + const { hasNextPage, endCursor } = r.orders.pageInfo; + after = hasNextPage ? endCursor : null; + pages++; +} while (after); + +// 2. Keep the five biggest customers +const top = [...byEmail] + .sort((a, b) => b[1].revenue - a[1].revenue) + .slice(0, 5); + +// 3. Join each one with its HubSpot contact +const result = []; +for (const [email, c] of top) { + const h = await amcp.run("hubspot_search_objects", { + object_type: "contacts", + filter_groups: [{ filters: [ + { propertyName: "email", operator: "EQ", value: email }, + ] }], + properties: ["lifecyclestage"], + limit: 1, + }); + const stage = h.results[0]?.properties.lifecyclestage ?? "not in CRM"; + result.push({ email, ...c, revenue: c.revenue.toFixed(2), stage }); +} +return { pagesScanned: pages, top: result }; +``` + +`code_execute` takes `{"code": ""}`. `jq -Rs` wraps the file for you: + +```console +$ jq -Rs '{code: .}' top-customers.js | amcp tools call code_execute --args - +{ + "pagesScanned": 3, + "top": [ + { + "email": "einkauf@kellerhaus.de", + "name": "Kellerhaus GmbH", + "orders": 14, + "revenue": "4820.50", + "stage": "customer" + }, + { + "email": "m.weber@example.com", + "name": "Martin Weber", + "orders": 6, + "revenue": "2475.90", + "stage": "not in CRM" + }, + … + ] +} +``` + +That is **8 tool calls** (3 pages of orders and 5 CRM lookups) for **one** call from your terminal. What comes back is the five customers you asked about, not 750 raw orders. The same holds when the caller is an AI agent instead of you: it reads one short answer instead of 750 orders, which saves tokens and avoids mistakes. + +Don't know the tool names yet? Let the program find them: + +```js +// discover.js +const tools = await amcp.search("invoice"); // [{ tool, description, readOnly }, …] +const schema = await amcp.describe(tools[0].tool); // { name, description, inputSchema, readOnly } +return { tools: tools.map(t => t.tool), firstSchema: schema.inputSchema }; ``` -A management key carries the owner's identity and role: it never grants privilege, so admin-only routes still need an admin owner, and read-only keys can only read. The API applies a per-key rate limit, so a runaway agent is refused rather than hammering your instance. +**The guardrails are part of the design:** -## Built for agents, too +- **Read-only.** A program can only call tools that don't change data, and only with the permissions of the key that runs it. Writes stay explicit direct calls, like `slack_post_message --yes` above. +- **Isolated.** The program runs in a separate sandbox process with no filesystem, network or environment access, a deadline, and a cap on tool calls per run. +- **Audited.** Every inner tool call is logged like a direct one, together with the code run that made it. +- **Opt-in.** Inline code is off unless a server's `codeMode` is `inline` *and* your role has the inline-code capability. It's available on the Team plan and above. -The same management API is exactly what a connected AI agent needs to **onboard and run a workspace end to end** — add a connector, create an MCP server, assign roles, wire SSO, toggle memory and skills — safely, within the scope you grant it. Agents default to read-only. +### Keep the good ones: propose them as tools -## Scripting and CI +When a program answers a question people keep asking, `code_propose_tool` records it as a proposal for an admin. It doesn't run anything. Once an admin approves it, every AI client on that server gets it as a normal named tool: + +```console +$ jq -Rs '{ + name: "top_customers_with_crm_stage", + description: "Top 5 Shopify customers since October, with their HubSpot lifecycle stage", + rationale: "Sales asks for this every Monday", + code: . + }' top-customers.js | amcp tools call code_propose_tool --args - +{"ok":true,"proposalId":"cmh4k2…","message":"Proposal recorded for admin review. It will not run or become a tool until an admin approves it."} +``` + +Read more in [Code mode for AI agents](/guides/code-mode). + +## 4. Manage the instance + +With a management key, `amcp api METHOD PATH` calls any `/api/*` route. It works like `gh api`, but for AnythingMCP. Text output is the parsed JSON body, so `jq` works here too. + +### Stand up a workspace in one script + +Install a catalogue connector with its token, give the sales team their own MCP server, attach the connector, turn on code mode and check the connection: ```bash -AMCP_URL=https://cloud.anythingmcp.com AMCP_API_KEY=mcp_… amcp api GET /api/users --json -amcp completion zsh > "${fpath[1]}/_amcp" # shell completion for bash, zsh, fish +#!/usr/bin/env bash +# bootstrap.sh — needs: amcp login --browser --scope admin +set -euo pipefail + +CONN=$(jq -n --arg t "$HUBSPOT_TOKEN" '{credentials: {HUBSPOT_ACCESS_TOKEN: $t}}' \ + | amcp api POST /api/adapters/hubspot/import --data - --yes | jq -r .connectorId) + +SRV=$(amcp api POST /api/mcp-servers --yes \ + --data '{"name":"Sales","description":"CRM tools for the sales team"}' | jq -r .id) + +amcp api PUT /api/mcp-servers/$SRV/connectors --yes --data "{\"connectorIds\":[\"$CONN\"]}" +amcp api PUT /api/mcp-servers/$SRV --yes --data '{"codeMode":"inline"}' +amcp api POST /api/connectors/$CONN/test --yes +# → { "ok": true, "kind": "ok", "message": "Connection successful" } ``` -Keys are stored with mode `0600`, HTTPS is required (except loopback for local dev), redirects are refused so a key can't follow a request to another origin, and the key is never accepted as a command argument. +The token reaches the API through `jq --arg` and stdin, so it never shows up in `ps` or your shell history. Every non-GET call in a script needs `--yes`. That is deliberate: a script can't change anything by accident. + +### Turn your own API into tools + +Not in the catalogue? Create a REST connector and import its OpenAPI spec. WSDL, GraphQL, Postman, cURL and HAR work the same way, through the `source` field: + +```bash +ID=$(amcp api POST /api/connectors --yes --data '{ + "name": "Acme Billing", "type": "REST", + "baseUrl": "https://api.acme.example/v1", + "authType": "BEARER_TOKEN", "authConfig": { "token": "'"$ACME_TOKEN"'" } +}' | jq -r .id) + +amcp api POST /api/connectors/$ID/import --yes \ + --data '{"source":"openapi","url":"https://api.acme.example/v1/openapi.json"}' +``` + +### See what your agents did + +Every tool call, from people or agents, lands in the audit log: + +```console +$ amcp api GET "/api/audit/invocations?status=ERROR&limit=3" \ + | jq -r '.[] | [.createdAt[11:16], .tool.name, .user.email, .error[:40]] | @tsv' +08:41 hubspot_search_objects lea@acme.io HTTP 429: retry after 1s +07:02 shopify_get_order ops-agent@acme.io Order not found +06:55 zendesk_get_ticket marco@acme.io HTTP 404: RecordNotFound +``` + +`GET /api/audit/breakdowns?days=30` returns usage and cost broken down by connector, MCP server and user. It's a good fit for a monthly report. + +### Discover the whole API offline + +You don't need the docs open. The catalogue of management operations ships with the CLI: + +```console +$ amcp api --list mcp-servers +mcp-servers + GET /api/mcp-servers — List MCP servers for current organization + POST /api/mcp-servers [body] — Create a new MCP server + DELETE /api/mcp-servers/:id — Delete MCP server + GET /api/mcp-servers/:id — Get MCP server detail + PUT /api/mcp-servers/:id [body] — Update MCP server + … +``` + +`[admin]` marks routes that need an admin owner, and `[body]` marks routes that take a request body. The catalogue is regenerated from the backend controllers, and CI fails when a route is missing, so it always matches your instance. The interactive reference with request examples is at [/developers/api](/developers/api). + +## 5. Give it to an agent + +A coding agent such as Claude Code, Codex or Cursor's agent can drive `amcp` like you do. Give it a **read-only** management key and it can audit a workspace: which servers expose which connectors, and which calls fail. Give it an **admin** key and it can onboard a new team end to end with the commands from section 4. + +The scope is the safety net. A management key carries its owner's identity and role and never grants more. A read-only key can only `GET`. Every call is rate-limited per key (HTTP 429 instead of a hammered instance) and audited. + +## Secure by default + +- The API key is never accepted as a command argument and never printed. `whoami` shows only a prefix. +- Profiles are stored in `~/.config/amcp/config.json` with file mode `0600`. +- HTTPS is required, except on loopback for local development. +- Redirects are refused, so a key can't follow a request to another origin. ## Get it -- **Install:** `npm i -g @anythingmcp/cli` -- **Docs:** the full command reference lives in the [CLI README on GitHub](https://github.com/HelpCode-ai/anythingmcp/tree/main/packages/cli#readme). -- **Source:** [github.com/HelpCode-ai/anythingmcp](https://github.com/HelpCode-ai/anythingmcp). +- **Install:** `npm i -g @anythingmcp/cli`, plus shell completion with `amcp completion zsh` (also bash and fish) +- **Overview:** [anythingmcp.com/developers/cli](/developers/cli) +- **Full command reference:** the [CLI README on GitHub](https://github.com/HelpCode-ai/anythingmcp/tree/main/packages/cli#readme) +- **Source:** [github.com/HelpCode-ai/anythingmcp](https://github.com/HelpCode-ai/anythingmcp) No AnythingMCP yet? Start free on [cloud.anythingmcp.com](https://cloud.anythingmcp.com), or self-host it. diff --git a/content/guides/it/anythingmcp-cli.mdx b/content/guides/it/anythingmcp-cli.mdx index 7a5ac0f2e..564a73514 100644 --- a/content/guides/it/anythingmcp-cli.mdx +++ b/content/guides/it/anythingmcp-cli.mdx @@ -1,83 +1,333 @@ --- title: "La CLI di AnythingMCP — Gestisci tutta la tua istanza MCP dal terminale" -description: "amcp è il client a riga di comando ufficiale di AnythingMCP. Chiama i tool MCP dalla shell e gestisci l'intera istanza — connettori, server MCP, utenti, ruoli, SSO e impostazioni — con una chiave API di gestione. Installa con npm, accedi dal browser, scrivi script per tutto. Per le persone e per gli agenti AI connessi." +description: "amcp è il client a riga di comando ufficiale di AnythingMCP. Chiama i tool MCP con argomenti reali, passa i risultati a jq e cron, esegui programmi in code mode che fanno decine di chiamate in un solo passo e automatizza l'intera istanza — connettori, server MCP, ruoli, audit — con una chiave di gestione. Esempi concreti con Shopify, HubSpot e Slack." category: "announcements" -keyword: "AnythingMCP CLI, amcp CLI, MCP CLI, Model Context Protocol CLI, MCP riga di comando, gestire server MCP da terminale, MCP gateway CLI, npm @anythingmcp/cli, tool MCP da terminale, CLI agente AI" +keyword: "AnythingMCP CLI, amcp CLI, MCP CLI, Model Context Protocol CLI, MCP riga di comando, MCP code mode, code_execute, gestire server MCP da terminale, MCP gateway CLI, npm @anythingmcp/cli, tool MCP da terminale, CLI agente AI" publishedAt: "2026-11-04" +updatedAt: "2026-11-05" lang: "it" featured: true --- ## Tutta la tua istanza MCP, dal terminale -Ecco **`amcp`**, il client a riga di comando ufficiale di AnythingMCP. Fa due cose: +**`amcp`** è il client a riga di comando ufficiale di AnythingMCP. Con un solo binario fa due cose: -1. **Chiama i tool MCP** dalla shell — scoprirli, leggerne lo schema, eseguirli. -2. **Guida l'API di gestione** (`/api/*`) con una **chiave API di gestione** — così una persona, o un agente AI connesso, può configurare l'intera istanza (connettori, server MCP, utenti, ruoli, SSO, memory, skill, knowledge graph e impostazioni) senza lasciare il terminale. +1. **Chiama i tool MCP** — gli stessi tool che usano i tuoi client AI (Shopify, HubSpot, SAP, il tuo database SQL, una cartella condivisa…). Puoi eseguirli a mano, negli script di shell, da cron e CI, oppure come **programma in code mode** che concatena decine di chiamate in un solo passo. +2. **Guida l'API di gestione** (`/api/*`) con una **chiave di gestione**. Tutto ciò che fa la dashboard — connettori, server MCP, utenti, ruoli, SSO, memory, skill, audit — diventa un comando, per te o per un agente connesso. -È open source (AGPL-3.0), funziona con **AnythingMCP Cloud** e con le istanze **self-hosted**, e si installa da npm: +È open source (AGPL-3.0), funziona con **AnythingMCP Cloud** e con le istanze **self-hosted**, richiede Node 22+ e si installa da npm: ```bash npm i -g @anythingmcp/cli amcp --help ``` -## Accedi +Ogni esempio qui sotto usa comandi, flag, endpoint e nomi di tool reali. I tool Shopify, HubSpot e Slack sono quelli dei connettori del catalogo. Solo i dati nell'output sono inventati. -Crea una **chiave API di gestione** nella dashboard (Impostazioni → Chiavi API di gestione, sola lettura o admin), oppure lascia che lo faccia la CLI tramite il browser: +## Accedi: due tipi di chiave + +`amcp` usa due tipi di chiave e li tiene in profili separati: + +| Vuoi… | Chiave | Accedi con | +|---|---|---| +| Chiamare i tool di un server MCP | una **chiave MCP** per quel server | `amcp login --url … --server SERVER_ID` | +| Gestire l'istanza (`amcp api`) | una **chiave di gestione** (sola lettura o admin) | `amcp login --browser` | + +Per una chiave di gestione, lascia fare al browser: ```bash -amcp login --browser --url https://cloud.anythingmcp.com # sola lettura -amcp login --browser --scope admin --url https://cloud.anythingmcp.com +amcp login --browser --url https://cloud.anythingmcp.com # read-only +amcp login --browser --scope admin --url https://cloud.anythingmcp.com # read + write ``` -`--browser` apre la pagina di autorizzazione dell'istanza, tu la approvi con la tua sessione della dashboard (SSO incluso), e la chiave torna alla CLI tramite un loopback `127.0.0.1` una tantum — non compare mai in un URL, in un log o nella cronologia della shell. Per collegarti invece a un singolo server MCP per le chiamate ai tool, usa `amcp login --url … --server …` con una chiave MCP. +La CLI apre la pagina di autorizzazione dell'istanza e stampa un breve codice. Controlli che corrisponda a quello della pagina e approvi con la tua normale sessione della dashboard (SSO incluso). La chiave torna alla CLI tramite un loopback `127.0.0.1` usa e getta, quindi non finisce mai in un URL, in un log o nella cronologia della shell. -## Chiama i tool MCP +Per chiamare i tool, incolla una chiave MCP quando richiesto. La CLI non la mostra a schermo e la verifica con una vera `tools/list` prima di salvarla: ```bash -amcp tools ls # elenca i tool, con indicazioni lettura/scrittura -amcp tools describe YOUR_TOOL # descrizione completa + schema JSON di input -amcp tools call YOUR_TOOL --args '{"id":"123"}' +amcp login --url https://cloud.anythingmcp.com --server cmob60dvv00c01zrpeujwmnep --profile shop +amcp whoami --profile shop +``` + +## 1. Esplora e chiama i tool + +Comincia chiedendo al server cosa sa fare. `tools ls` stampa per ogni tool il nome, l'indicazione sola lettura/distruttivo e la prima riga della descrizione: + +```console +$ amcp tools ls --filter order +shopify_search_orders read-only Search orders with Shopify search syntax (for example `financial_status:paid … +shopify_get_order read-only Get one order: status, totals (subtotal, shipping, tax, discounts, refunds) … +``` + +`tools describe` mostra la descrizione completa e lo schema JSON di input, così sai esattamente quali argomenti accetta un tool. Poi lo chiami con argomenti reali: + +```console +$ amcp tools call shopify_search_orders \ + --args '{"query":"financial_status:paid created_at:>=2026-11-01","first":2}' +{ + "orders": { + "pageInfo": { "hasNextPage": true, "endCursor": "eyJsYXN0X2lkIjo2…" }, + "nodes": [ + { + "name": "#1043", + "createdAt": "2026-11-04T16:12:09Z", + "displayFinancialStatus": "PAID", + "displayFulfillmentStatus": "UNFULFILLED", + "currentTotalPriceSet": { "shopMoney": { "amount": "412.80", "currencyCode": "EUR" } }, + "customer": { "displayName": "Kellerhaus GmbH" } + }, + … +``` + +Gli argomenti possono arrivare anche da un file (`--args @args.json`) o da stdin (`--args -`). Un tool marcato come distruttivo chiede conferma nel terminale. In uno script devi passare `--yes` esplicitamente. + +## 2. Usalo nei tuoi script + +L'output testuale di un tool è il suo JSON su stdout. Gli errori vanno su stderr con un exit code proprio. Così `amcp` si comporta come qualsiasi comando Unix: + +```console +$ amcp tools call shopify_search_orders \ + --args '{"query":"fulfillment_status:unfulfilled created_at:<2026-11-03"}' \ + | jq -r '.orders.nodes[] | [.name, .customer.displayName, .createdAt[:10]] | @tsv' +#1038 Kellerhaus GmbH 2026-10-30 +#1035 Studio Lumen 2026-10-29 +#1031 Bäckerei Hofmann 2026-10-28 ``` -## Gestisci l'istanza +Exit code: `0` successo, `1` il tool ha restituito un errore, `2` uso o JSON non validi, `3` autenticazione, `4` connessione, errore del server o timeout. -Tutto ciò che fa la dashboard lo fa `amcp api` con una chiave di gestione — è il `gh api` di AnythingMCP: +Mettendo insieme le due cose ottieni un job quotidiano. Questo pubblica ogni mattina in un canale Slack gli ordini in ritardo (il connettore Slack è sullo stesso server): ```bash -amcp api GET /api/connectors -amcp api GET /api/mcp-servers -amcp api POST /api/mcp-servers --data '{"name":"ops","description":"Server ops"}' -amcp api PUT /api/mcp-servers/SERVER_ID --data '{"codeMode":"tools"}' +#!/usr/bin/env bash +# late-orders.sh — run from cron: 0 8 * * 1-5 +set -euo pipefail +SINCE=$(date -d '-2 days' +%F) # GNU date; on macOS: date -v-2d +%F +LATE=$(amcp tools call shopify_search_orders \ + --args "{\"query\":\"fulfillment_status:unfulfilled created_at:<$SINCE\"}" \ + | jq -r '.orders.nodes[] | "• \(.name) — \(.customer.displayName)"') +[ -z "$LATE" ] && exit 0 +jq -n --arg text "Orders waiting more than 2 days: +$LATE" '{channel: "#fulfilment", text: $text}' \ + | amcp tools call slack_post_message --args - --yes ``` -Non sai cosa è disponibile? La CLI include un catalogo di tutte le operazioni di gestione — senza chiave, senza rete: +In CI puoi fare a meno del profilo: `AMCP_URL`, `AMCP_SERVER` e `AMCP_API_KEY` forniscono la connessione, e `--json` fa stampare a ogni comando un unico oggetto JSON. + +## 3. Code mode: un programma, tante chiamate + +Script come quello sopra fanno una chiamata alla volta dalla tua macchina. Il **code mode** invece manda al server un programma intero. Con il code mode impostato su `inline` per un server MCP, il server offre un tool chiamato `code_execute`. Gli mandi un breve programma JavaScript e una sandbox accanto ai tuoi tool lo esegue, con tre funzioni a disposizione: + +- `await amcp.search("query")` cerca i tool per nome o descrizione; +- `await amcp.describe("tool")` restituisce lo schema di input di un tool; +- `await amcp.run("tool", args)` chiama un tool e restituisce il risultato come valore JavaScript. + +Si attiva una volta per server (serve una chiave di gestione admin): ```bash -amcp api --list # tutte le operazioni, raggruppate per risorsa -amcp api --list mcp-servers # solo una risorsa +amcp api PUT /api/mcp-servers/$SRV --yes --data '{"codeMode":"inline"}' +``` + +Ora una domanda che richiederebbe decine di passaggi: *quali sono i nostri cinque clienti Shopify più grandi da ottobre, e a che punto è ognuno nella pipeline di HubSpot?* Scrivila come programma: + +```js +// top-customers.js — runs inside the AnythingMCP sandbox: read-only, your own permissions +const byEmail = new Map(); +let after = null, pages = 0; + +// 1. Page through every paid Shopify order since October +do { + const r = await amcp.run("shopify_search_orders", { + query: "financial_status:paid created_at:>=2026-10-01", + first: 250, after, + }); + for (const o of r.orders.nodes) { + if (!o.email) continue; + const c = byEmail.get(o.email) + ?? { name: o.customer?.displayName, orders: 0, revenue: 0 }; + c.orders += 1; + c.revenue += Number(o.currentTotalPriceSet.shopMoney.amount); + byEmail.set(o.email, c); + } + const { hasNextPage, endCursor } = r.orders.pageInfo; + after = hasNextPage ? endCursor : null; + pages++; +} while (after); + +// 2. Keep the five biggest customers +const top = [...byEmail] + .sort((a, b) => b[1].revenue - a[1].revenue) + .slice(0, 5); + +// 3. Join each one with its HubSpot contact +const result = []; +for (const [email, c] of top) { + const h = await amcp.run("hubspot_search_objects", { + object_type: "contacts", + filter_groups: [{ filters: [ + { propertyName: "email", operator: "EQ", value: email }, + ] }], + properties: ["lifecyclestage"], + limit: 1, + }); + const stage = h.results[0]?.properties.lifecyclestage ?? "not in CRM"; + result.push({ email, ...c, revenue: c.revenue.toFixed(2), stage }); +} +return { pagesScanned: pages, top: result }; +``` + +`code_execute` accetta `{"code": ""}`. `jq -Rs` impacchetta il file per te: + +```console +$ jq -Rs '{code: .}' top-customers.js | amcp tools call code_execute --args - +{ + "pagesScanned": 3, + "top": [ + { + "email": "einkauf@kellerhaus.de", + "name": "Kellerhaus GmbH", + "orders": 14, + "revenue": "4820.50", + "stage": "customer" + }, + { + "email": "m.weber@example.com", + "name": "Martin Weber", + "orders": 6, + "revenue": "2475.90", + "stage": "not in CRM" + }, + … + ] +} +``` + +Sono **8 chiamate ai tool** (3 pagine di ordini e 5 ricerche nel CRM) per **una** sola chiamata dal tuo terminale. In risposta arrivano i cinque clienti che hai chiesto, non 750 ordini grezzi. Lo stesso vale quando a chiamare è un agente AI invece di te: legge una risposta breve invece di 750 ordini, così risparmia token e sbaglia meno. + +Non conosci ancora i nomi dei tool? Lascia che li trovi il programma: + +```js +// discover.js +const tools = await amcp.search("invoice"); // [{ tool, description, readOnly }, …] +const schema = await amcp.describe(tools[0].tool); // { name, description, inputSchema, readOnly } +return { tools: tools.map(t => t.tool), firstSchema: schema.inputSchema }; ``` -Una chiave di gestione porta con sé identità e ruolo del proprietario: non concede mai privilegi, quindi le rotte solo-admin richiedono comunque un proprietario admin, e le chiavi in sola lettura possono solo leggere. L'API applica un rate limit per chiave, così un agente impazzito viene respinto invece di martellare la tua istanza. +**Le protezioni fanno parte del progetto:** -## Pensato anche per gli agenti +- **Sola lettura.** Un programma può chiamare solo tool che non modificano dati, e solo con i permessi della chiave che lo esegue. Le scritture restano chiamate dirette ed esplicite, come `slack_post_message --yes` sopra. +- **Isolato.** Il programma gira in un processo sandbox separato, senza filesystem, rete né variabili d'ambiente, con un tempo massimo e un limite di chiamate per esecuzione. +- **Tracciato.** Ogni chiamata interna finisce nel log come una diretta, insieme all'esecuzione che l'ha fatta. +- **Su richiesta.** Il codice inline è spento finché il `codeMode` del server non è `inline` *e* il tuo ruolo non ha la capacità di codice inline. È disponibile dal piano Team in su. -La stessa API di gestione è esattamente ciò che serve a un agente AI connesso per **configurare e far funzionare un workspace end-to-end** — aggiungere un connettore, creare un server MCP, assegnare ruoli, impostare l'SSO, attivare memory e skill — in sicurezza, nei limiti dell'ambito che gli concedi. Gli agenti sono in sola lettura per impostazione predefinita. +### Tieni quelli buoni: proponili come tool -## Script e CI +Quando un programma risponde a una domanda che torna spesso, `code_propose_tool` lo registra come proposta per un admin. Non esegue nulla. Quando un admin la approva, tutti i client AI di quel server lo ricevono come un normale tool con un nome: + +```console +$ jq -Rs '{ + name: "top_customers_with_crm_stage", + description: "Top 5 Shopify customers since October, with their HubSpot lifecycle stage", + rationale: "Sales asks for this every Monday", + code: . + }' top-customers.js | amcp tools call code_propose_tool --args - +{"ok":true,"proposalId":"cmh4k2…","message":"Proposal recorded for admin review. It will not run or become a tool until an admin approves it."} +``` + +Approfondisci in [Code mode per agenti AI](/it/guides/code-mode). + +## 4. Gestisci l'istanza + +Con una chiave di gestione, `amcp api METODO PERCORSO` chiama qualsiasi rotta `/api/*`. Funziona come `gh api`, ma per AnythingMCP. L'output testuale è il body JSON già interpretato, quindi anche qui funziona `jq`. + +### Configura un workspace in uno script + +Installa un connettore del catalogo con il suo token, dai al team vendite un proprio server MCP, collega il connettore, attiva il code mode e verifica la connessione: ```bash -AMCP_URL=https://cloud.anythingmcp.com AMCP_API_KEY=mcp_… amcp api GET /api/users --json -amcp completion zsh > "${fpath[1]}/_amcp" # completamento shell per bash, zsh, fish +#!/usr/bin/env bash +# bootstrap.sh — needs: amcp login --browser --scope admin +set -euo pipefail + +CONN=$(jq -n --arg t "$HUBSPOT_TOKEN" '{credentials: {HUBSPOT_ACCESS_TOKEN: $t}}' \ + | amcp api POST /api/adapters/hubspot/import --data - --yes | jq -r .connectorId) + +SRV=$(amcp api POST /api/mcp-servers --yes \ + --data '{"name":"Sales","description":"CRM tools for the sales team"}' | jq -r .id) + +amcp api PUT /api/mcp-servers/$SRV/connectors --yes --data "{\"connectorIds\":[\"$CONN\"]}" +amcp api PUT /api/mcp-servers/$SRV --yes --data '{"codeMode":"inline"}' +amcp api POST /api/connectors/$CONN/test --yes +# → { "ok": true, "kind": "ok", "message": "Connection successful" } ``` -Le chiavi sono salvate con modalità `0600`, l'HTTPS è obbligatorio (tranne il loopback per lo sviluppo locale), i redirect vengono rifiutati così una chiave non può seguire una richiesta verso un'altra origine, e la chiave non è mai accettata come argomento del comando. +Il token arriva all'API tramite `jq --arg` e stdin, quindi non compare mai in `ps` né nella cronologia della shell. In uno script ogni chiamata che non sia GET richiede `--yes`. È voluto: uno script non può cambiare niente per sbaglio. + +### Trasforma la tua API in tool + +Non è nel catalogo? Crea un connettore REST e importa la sua specifica OpenAPI. WSDL, GraphQL, Postman, cURL e HAR funzionano allo stesso modo, tramite il campo `source`: + +```bash +ID=$(amcp api POST /api/connectors --yes --data '{ + "name": "Acme Billing", "type": "REST", + "baseUrl": "https://api.acme.example/v1", + "authType": "BEARER_TOKEN", "authConfig": { "token": "'"$ACME_TOKEN"'" } +}' | jq -r .id) + +amcp api POST /api/connectors/$ID/import --yes \ + --data '{"source":"openapi","url":"https://api.acme.example/v1/openapi.json"}' +``` + +### Guarda cosa hanno fatto i tuoi agenti + +Ogni chiamata ai tool, di persone o di agenti, finisce nel log di audit: + +```console +$ amcp api GET "/api/audit/invocations?status=ERROR&limit=3" \ + | jq -r '.[] | [.createdAt[11:16], .tool.name, .user.email, .error[:40]] | @tsv' +08:41 hubspot_search_objects lea@acme.io HTTP 429: retry after 1s +07:02 shopify_get_order ops-agent@acme.io Order not found +06:55 zendesk_get_ticket marco@acme.io HTTP 404: RecordNotFound +``` + +`GET /api/audit/breakdowns?days=30` restituisce utilizzo e costi suddivisi per connettore, server MCP e utente. È adatto per un report mensile. + +### Scopri tutta l'API offline + +Non serve avere la documentazione aperta. Il catalogo delle operazioni di gestione è incluso nella CLI: + +```console +$ amcp api --list mcp-servers +mcp-servers + GET /api/mcp-servers — List MCP servers for current organization + POST /api/mcp-servers [body] — Create a new MCP server + DELETE /api/mcp-servers/:id — Delete MCP server + GET /api/mcp-servers/:id — Get MCP server detail + PUT /api/mcp-servers/:id [body] — Update MCP server + … +``` + +`[admin]` indica le rotte che richiedono un proprietario admin, `[body]` quelle che accettano un body. Il catalogo viene rigenerato dai controller del backend e la CI fallisce se manca una rotta, quindi corrisponde sempre alla tua istanza. Il riferimento interattivo con esempi di richiesta è su [/developers/api](/it/developers/api). + +## 5. Affidala a un agente + +Un agente di coding come Claude Code, Codex o l'agente di Cursor può usare `amcp` come fai tu. Con una chiave di gestione **in sola lettura** può controllare un workspace: quali server espongono quali connettori, quali chiamate falliscono. Con una chiave **admin** può configurare un nuovo team da capo a fondo con i comandi della sezione 4. + +L'ambito della chiave è la rete di sicurezza. Una chiave di gestione porta identità e ruolo del suo proprietario e non concede mai di più. Una chiave in sola lettura può solo fare `GET`. Ogni chiamata ha un rate limit per chiave (HTTP 429 invece di un'istanza sovraccarica) ed è tracciata. + +## Sicura per default + +- La chiave non è mai accettata come argomento del comando né stampata. `whoami` ne mostra solo il prefisso. +- I profili sono salvati in `~/.config/amcp/config.json` con modalità `0600`. +- L'HTTPS è obbligatorio, tranne il loopback per lo sviluppo locale. +- I redirect vengono rifiutati, così una chiave non può seguire una richiesta verso un'altra origine. ## Dove prenderla -- **Installa:** `npm i -g @anythingmcp/cli` -- **Doc:** il riferimento completo dei comandi è nel [README della CLI su GitHub](https://github.com/HelpCode-ai/anythingmcp/tree/main/packages/cli#readme). -- **Sorgente:** [github.com/HelpCode-ai/anythingmcp](https://github.com/HelpCode-ai/anythingmcp). +- **Installa:** `npm i -g @anythingmcp/cli`, più il completamento della shell con `amcp completion zsh` (anche bash e fish) +- **Panoramica:** [anythingmcp.com/developers/cli](/it/developers/cli) +- **Riferimento completo dei comandi:** il [README della CLI su GitHub](https://github.com/HelpCode-ai/anythingmcp/tree/main/packages/cli#readme) +- **Sorgente:** [github.com/HelpCode-ai/anythingmcp](https://github.com/HelpCode-ai/anythingmcp) -Non hai ancora AnythingMCP? Inizia gratis su [cloud.anythingmcp.com](https://cloud.anythingmcp.com), o fai self-hosting. +Non hai ancora AnythingMCP? Inizia gratis su [cloud.anythingmcp.com](https://cloud.anythingmcp.com), oppure fai self-hosting. diff --git a/packages/backend/src/adapters/adapters.controller.ts b/packages/backend/src/adapters/adapters.controller.ts index 05d3727f5..6ccd2b700 100644 --- a/packages/backend/src/adapters/adapters.controller.ts +++ b/packages/backend/src/adapters/adapters.controller.ts @@ -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'; @@ -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, @@ -118,6 +150,7 @@ export class AdaptersPublicController { // Authenticated endpoints @ApiTags('Adapters') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/adapters') export class AdaptersController { @@ -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); @@ -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, @@ -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, diff --git a/packages/backend/src/adapters/setup-links.controller.ts b/packages/backend/src/adapters/setup-links.controller.ts index 4eed2f62d..297a79519 100644 --- a/packages/backend/src/adapters/setup-links.controller.ts +++ b/packages/backend/src/adapters/setup-links.controller.ts @@ -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'; @@ -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' }) diff --git a/packages/backend/src/alerts/alerts.controller.ts b/packages/backend/src/alerts/alerts.controller.ts index 88f7fce4d..67590ca0b 100644 --- a/packages/backend/src/alerts/alerts.controller.ts +++ b/packages/backend/src/alerts/alerts.controller.ts @@ -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'; @@ -54,6 +54,7 @@ class SaveAlertWebhookDto { @ApiTags('Alerts') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @Controller('api/admin/settings') diff --git a/packages/backend/src/audit/audit.controller.ts b/packages/backend/src/audit/audit.controller.ts index 28c9be391..dd0558424 100644 --- a/packages/backend/src/audit/audit.controller.ts +++ b/packages/backend/src/audit/audit.controller.ts @@ -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 { @@ -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, @@ -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); } @@ -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', @@ -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, diff --git a/packages/backend/src/auth/login.controller.ts b/packages/backend/src/auth/login.controller.ts index 99cf29f04..406703916 100644 --- a/packages/backend/src/auth/login.controller.ts +++ b/packages/backend/src/auth/login.controller.ts @@ -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. @@ -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); diff --git a/packages/backend/src/common/openapi.spec.ts b/packages/backend/src/common/openapi.spec.ts new file mode 100644 index 000000000..6f1faa499 --- /dev/null +++ b/packages/backend/src/common/openapi.spec.ts @@ -0,0 +1,67 @@ +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import { APP_VERSION } from './app-version'; +import { API_KEY_SCHEME, buildOpenApiConfig, OPENAPI_TAGS } from './openapi'; + +const SRC = join(__dirname, '..'); + +function controllerFiles(dir: string): string[] { + const out: string[] = []; + for (const name of readdirSync(dir)) { + const p = join(dir, name); + if (statSync(p).isDirectory()) out.push(...controllerFiles(p)); + else if (name.endsWith('.controller.ts')) out.push(p); + } + return out; +} + +/** Every tag named in an `@ApiTags(...)` decorator under src/. */ +function usedTags(): Set { + const tags = new Set(); + for (const file of controllerFiles(SRC)) { + const text = readFileSync(file, 'utf8'); + for (const call of text.matchAll(/@ApiTags\(([^)]*)\)/g)) { + for (const lit of call[1].matchAll(/['"]([^'"]+)['"]/g)) tags.add(lit[1]); + } + } + return tags; +} + +describe('OpenAPI document config', () => { + const config = buildOpenApiConfig(); + + it('declares every tag a controller uses, and none that no controller uses', () => { + const declared = OPENAPI_TAGS.map((t) => t.name); + const used = usedTags(); + expect(used.size).toBeGreaterThan(0); + expect(declared.filter((t) => !used.has(t))).toEqual([]); + expect([...used].filter((t) => !declared.includes(t))).toEqual([]); + expect(new Set(declared).size).toBe(declared.length); + }); + + it('gives every tag a description and passes them to the document in order', () => { + for (const tag of OPENAPI_TAGS) expect(tag.description.trim()).not.toBe(''); + expect(config.tags?.map((t) => t.name)).toEqual(OPENAPI_TAGS.map((t) => t.name)); + }); + + it('reports the running version, not a hard-coded one', () => { + expect(config.info.version).toBe(APP_VERSION); + expect(buildOpenApiConfig('9.9.9').info.version).toBe('9.9.9'); + }); + + it('declares the bearer scheme and the X-API-Key scheme the controllers reference', () => { + const schemes = config.components?.securitySchemes ?? {}; + expect(schemes.bearer).toMatchObject({ type: 'http', scheme: 'bearer' }); + expect(schemes[API_KEY_SCHEME]).toMatchObject({ type: 'apiKey', in: 'header', name: 'X-API-Key' }); + }); + + it('has a description with authentication, quick start and errors, plus license and links', () => { + const d = config.info.description ?? ''; + for (const heading of ['## Authentication', '## Quick start', '## Rate limits', '## Errors']) { + expect(d).toContain(heading); + } + for (const scope of ['MANAGEMENT_READ', 'MANAGEMENT_ADMIN', '`MCP`']) expect(d).toContain(scope); + expect(config.info.license?.name).toBe('AGPL-3.0'); + expect(config.externalDocs?.url).toMatch(/^https:\/\/anythingmcp\.com\//); + }); +}); diff --git a/packages/backend/src/common/openapi.ts b/packages/backend/src/common/openapi.ts new file mode 100644 index 000000000..513c59024 --- /dev/null +++ b/packages/backend/src/common/openapi.ts @@ -0,0 +1,345 @@ +import { DocumentBuilder, type OpenAPIObject } from '@nestjs/swagger'; +import { APP_VERSION } from './app-version'; + +/** + * The OpenAPI document's top-level metadata, served at /api/docs (Swagger UI) + * and /api/docs-json. Kept out of main.ts so it can be unit-tested and + * rendered without booting the app. + * + * Every claim in OPENAPI_DESCRIPTION is backed by code; keep it that way when + * behaviour changes: + * - auth and key scopes: auth/jwt-or-management-key.guard.ts + * - per-key rate limit: same file (MANAGEMENT_RATE_LIMIT_PER_MINUTE) + * - per-IP rate limit: app.module.ts (ThrottlerModule.forRoot) + * - key minting rules: roles/mcp-api-keys.controller.ts + * - code mode values: mcp-server/code-tool.util.ts (CODE_MODE_MODES) + */ + +export const OPENAPI_REPO_URL = 'https://github.com/HelpCode-ai/anythingmcp'; +export const OPENAPI_API_DOCS_URL = 'https://anythingmcp.com/developers/api'; +export const OPENAPI_CLI_DOCS_URL = 'https://anythingmcp.com/developers/cli'; + +/** Name of the `X-API-Key` security scheme, referenced by `@ApiSecurity('api-key')`. */ +export const API_KEY_SCHEME = 'api-key'; + +/** + * Every tag a controller uses in `@ApiTags(...)`, in the order Swagger UI shows + * them: the management resources integrators and the `amcp` CLI use most come + * first. openapi.spec.ts fails when a controller uses a tag missing here, or + * when a tag here is used by no controller. + */ +export const OPENAPI_TAGS: ReadonlyArray<{ name: string; description: string }> = [ + { + name: 'Connectors', + description: + 'The systems AnythingMCP calls (REST, SOAP, GraphQL, OData, databases, mail, remote MCP servers): create, import tools from a spec, test, health-check, export and restore.', + }, + { + name: 'Adapters', + description: + 'The built-in connector catalogue: browse it, read an adapter, verify credentials, and install it as a ready-to-use connector in one call.', + }, + { + name: 'Tools', + description: + 'The MCP tools of one connector: create, edit, enable or disable, annotations, response mapping and proxy settings.', + }, + { + name: 'MCP Servers', + description: + 'MCP endpoints (`/mcp/`), each exposing a chosen set of connectors to AI clients, with its own instructions, skills delivery and code mode.', + }, + { + name: 'MCP API Keys', + description: + 'Your personal `mcp_…` keys: `MCP` scope for AI clients, `MANAGEMENT_READ` / `MANAGEMENT_ADMIN` for this API and the `amcp` CLI.', + }, + { + name: 'MCP Connections', + description: + 'AI clients connected over OAuth to the shared `/mcp` endpoint, and which workspaces and MCP servers each one may reach.', + }, + { + name: 'MCP Endpoint', + description: + 'The Model Context Protocol endpoints AI clients connect to (Streamable HTTP, JSON-RPC): `/mcp/{serverId}` per server, the shared `/mcp`, and `/mcp/demo`. Use an MCP client, not plain REST calls; sign in with OAuth or send an MCP API key.', + }, + { + name: 'Roles', + description: + 'Role-based access to tools: which tools each MCP role may call and which roles each member holds. Admin only.', + }, + { + name: 'Users', + description: + 'Your own profile, and (admin) the workspace members, invitations, roles, sessions and deactivation.', + }, + { + name: 'Audit', + description: + 'The log of every tool call with input, output, status and duration, plus usage statistics, daily analytics and per-connector / server / user breakdowns.', + }, + { + name: 'Organizations', + description: + 'Workspaces: the ones you belong to, switching between them, creating one (Cloud), and updating or deleting the current one.', + }, + { + name: 'Identity Providers', + description: + 'Single sign-on providers (Entra ID, Google, Okta, Auth0, GitHub, generic OIDC), group-to-role mappings, SSO enforcement and SCIM provisioning. Admin, self-hosted.', + }, + { + name: 'SSO', + description: 'Signing in through an identity provider, and linking one to your own account.', + }, + { + name: 'Auth', + description: + 'Dashboard sign-up, sign-in, sessions, email verification, password reset and social sign-in.', + }, + { + name: 'Memory', + description: + 'Personal and company memory that every AI client shares: items, approval of company items, and access settings.', + }, + { + name: 'Knowledge Graph', + description: + 'How your connectors and tools relate, and the AI skills (procedures) agents follow: suggest, generate, create, apply or dismiss.', + }, + { + name: 'Knowledge folders', + description: 'Folders that organise skills and company memory, optionally limited to roles.', + }, + { + name: 'File Search', + description: + 'File Search connectors over SFTP, FTP/FTPS, WebDAV, S3 or a mounted network share: test, save, scan, choose what to index, and policy.', + }, + { + name: 'File Share', + description: + 'Temporary download links for files from your folders or written by the AI, internal or public, with limits and revoke.', + }, + { + name: 'Connector setup', + description: + 'Setup links that let a member finish a connector configuration (credentials, sign-in) in the dashboard.', + }, + { + name: 'MCP OAuth', + description: + 'OAuth redirect targets and completion for connectors that sign in at the provider, shared or per user.', + }, + { + name: 'MCP client sign-in', + description: + 'Browser pages of the OAuth flow in which an AI client signs in to AnythingMCP and the user picks what it may reach.', + }, + { + name: 'OAuth discovery', + description: + 'OAuth and OpenID discovery documents (`/.well-known/…`) that MCP clients read before signing in.', + }, + { + name: 'Shopee', + description: 'Shop authorization for Shopee connectors.', + }, + { + name: 'TikTok Shop', + description: 'Shop authorization for TikTok Shop connectors.', + }, + { + name: 'Alerts', + description: 'The webhook notified when a connector starts failing. Admin only.', + }, + { + name: 'Site Settings', + description: + 'Workspace and instance settings: SMTP, footer links and the SSRF allowlist (admin), plus the public footer links.', + }, + { + name: 'License', + description: + 'Edition, licence and plan status, usage against plan limits, trials and billing links.', + }, + { + name: 'Support', + description: 'Support chat widget settings and its inbound webhook.', + }, + { + name: 'Health', + description: 'Liveness / readiness check (`/health`) and public server information.', + }, + { + name: 'Version', + description: 'Version, commit and build date of this instance.', + }, + { + name: 'Public', + description: 'Public aggregate numbers, for the website.', + }, + { + name: 'Product events', + description: 'Product-usage events recorded by the dashboard. Internal.', + }, + { + name: 'Cron', + description: 'Scheduled jobs triggered by the AnythingMCP Cloud scheduler. Internal.', + }, +]; + +export const OPENAPI_DESCRIPTION = ` +**AnythingMCP** is an open-source agent harness and MCP gateway. It turns any REST/OpenAPI, SOAP/WSDL, GraphQL or OData API, SaaS app, SQL or NoSQL database, mail server or file share (SFTP, FTP/FTPS, WebDAV, S3, a mounted network share) into [Model Context Protocol](https://modelcontextprotocol.io) tools that AI agents such as Claude, ChatGPT, Copilot, Cursor and Gemini can call, with governance on top. + +This API is what the dashboard and the [\`amcp\` CLI](${OPENAPI_CLI_DOCS_URL}) use. With it you can: + +- **Connectors**: install a ready-made connector from the built-in catalogue (\`/api/adapters\`), or build your own and import its tools from OpenAPI, WSDL, GraphQL, OData, Postman, cURL, HAR, JSON or another MCP server. +- **MCP servers**: publish MCP endpoints at \`/mcp/\`, each exposing a chosen set of connectors, with instructions, skills and **code mode** (\`codeMode\`: \`off\` no code execution, \`tools\` saved code tools only, \`inline\` also the generic \`code_execute\` / \`code_propose_tool\`). +- **Governance**: roles with per-tool access, SSO and SCIM, per-user credentials and OAuth sign-in, an audit log of every tool call with usage analytics. +- **Company brain**: memory, AI skills, the knowledge graph, File Search and File Share. + +AnythingMCP runs on [AnythingMCP Cloud](https://cloud.anythingmcp.com) or self-hosted (AGPL-3.0, [source on GitHub](${OPENAPI_REPO_URL})). Cloud base URL: \`https://cloud.anythingmcp.com\`; a self-hosted backend listens on \`http://localhost:4000\` by default. + +## Authentication + +Routes marked with a lock need one of these credentials: + +| Credential | How to send it | Notes | +|---|---|---| +| Management API key \`mcp_…\` | \`Authorization: Bearer mcp_…\` **or** \`X-API-Key: mcp_…\` | Recommended for scripts, CI and agents. | +| Dashboard session JWT | \`Authorization: Bearer \` | The \`accessToken\` returned by \`POST /api/auth/login\`. | + +Key scopes: + +- \`MCP\`: tool calls on \`/mcp\` only. **Rejected on \`/api\`** (401). +- \`MANAGEMENT_READ\`: \`GET\` and \`HEAD\` only; any other method gets 403. +- \`MANAGEMENT_ADMIN\`: any method. Only an admin can create one. + +A key acts as its owner, with the owner's role in the key's workspace. The scope only narrows what the owner can do and never adds to it: routes marked *(ADMIN)* still need an admin owner, and a \`VIEWER\` still cannot change connectors. + +Create a key with \`POST /api/mcp-keys\` (\`{"name": "ci", "scope": "MANAGEMENT_READ"}\`), in the dashboard, or with \`amcp login --browser --url [--scope admin]\`. The full key is returned only once. + +An API key works on the routes that list the \`api-key\` scheme. The routes of the **Auth**, **SSO**, **MCP OAuth**, **Shopee**, **TikTok Shop** and **Support** tags, and a few others, accept only a dashboard session. Routes without a lock need no credential, except a few browser redirect and completion routes of the MCP OAuth, Shopee and TikTok Shop tags. + +## Quick start + +\`\`\`bash +export AMCP_URL=https://cloud.anythingmcp.com # or your self-hosted URL +export AMCP_API_KEY=mcp_… # a MANAGEMENT_ADMIN key + +# Who am I? +curl -s "$AMCP_URL/api/users/me" -H "Authorization: Bearer $AMCP_API_KEY" + +# List connectors (paginated: limit 1..200, offset) +curl -s "$AMCP_URL/api/connectors?limit=50" -H "X-API-Key: $AMCP_API_KEY" + +# Read a catalogue adapter (required variables, tools), then install it +curl -s "$AMCP_URL/api/adapters/hubspot" -H "Authorization: Bearer $AMCP_API_KEY" +curl -s -X POST "$AMCP_URL/api/adapters/hubspot/import" \\ + -H "Authorization: Bearer $AMCP_API_KEY" -H "Content-Type: application/json" \\ + -d '{"credentials": {"HUBSPOT_ACCESS_TOKEN": "pat-eu1-…"}}' +# → {"connectorId": "…", "toolsCreated": 14, "attachedToServer": {…}, "probe": {"ok": true, …}} + +# Create an MCP server and put connectors on it (the list replaces the current one) +curl -s -X POST "$AMCP_URL/api/mcp-servers" \\ + -H "Authorization: Bearer $AMCP_API_KEY" -H "Content-Type: application/json" \\ + -d '{"name": "Sales", "description": "CRM tools for the sales team"}' +curl -s -X PUT "$AMCP_URL/api/mcp-servers//connectors" \\ + -H "Authorization: Bearer $AMCP_API_KEY" -H "Content-Type: application/json" \\ + -d '{"connectorIds": [""]}' + +# Turn on code mode for that server +curl -s -X PUT "$AMCP_URL/api/mcp-servers/" \\ + -H "Authorization: Bearer $AMCP_API_KEY" -H "Content-Type: application/json" \\ + -d '{"codeMode": "inline"}' + +# Mint an MCP key for an AI client; it then connects to $AMCP_URL/mcp/ +curl -s -X POST "$AMCP_URL/api/mcp-keys" \\ + -H "Authorization: Bearer $AMCP_API_KEY" -H "Content-Type: application/json" \\ + -d '{"name": "claude-desktop", "mcpServerId": "", "scope": "MCP"}' + +# The last 20 failed tool calls +curl -s "$AMCP_URL/api/audit/invocations?status=ERROR&limit=20" -H "Authorization: Bearer $AMCP_API_KEY" +\`\`\` + +The same with the CLI (\`npm i -g @anythingmcp/cli\`). \`amcp api --list\` prints every operation it can call, offline: + +\`\`\`bash +amcp login --browser --url https://cloud.anythingmcp.com --scope admin +amcp api GET /api/users/me +amcp api GET "/api/connectors?limit=50" +amcp api POST /api/adapters/hubspot/import --data '{"credentials": {"HUBSPOT_ACCESS_TOKEN": "pat-eu1-…"}}' --yes +amcp api POST /api/mcp-servers --data '{"name": "Sales"}' --yes +amcp api PUT /api/mcp-servers//connectors --data '{"connectorIds": [""]}' --yes +amcp api PUT /api/mcp-servers/ --data '{"codeMode": "inline"}' --yes +amcp api GET "/api/audit/invocations?status=ERROR&limit=20" +\`\`\` + +In CI, set \`AMCP_URL\` and \`AMCP_API_KEY\` instead of logging in. The CLI asks before any method other than \`GET\`; \`--yes\` skips the question. + +## Rate limits + +- Per client IP: 100 requests a minute across the API (a few routes allow less, and the public adapter catalogue is not limited). +- Per management API key: 300 requests a minute by default (\`MANAGEMENT_RATE_LIMIT_PER_MINUTE\` on self-hosted; \`0\` turns it off). + +Over a limit the API answers **429**; wait for the time in the \`Retry-After\` header (or the \`retryAfter\` field) before retrying. + +## Errors + +Errors are JSON: \`{"statusCode": 404, "message": "MCP server not found", "error": "Not Found"}\`. + +| Status | Meaning | +|---|---| +| 400 | Invalid body or query. Unknown fields are rejected; \`message\` lists what is wrong. | +| 401 | Missing, invalid or revoked credential, or an \`MCP\`-scoped key used on \`/api\`. | +| 403 | Authenticated but not allowed: a read-only key on a write, a \`VIEWER\` changing something, a non-admin on an *(ADMIN)* route, a plan limit, or a connector of another workspace. | +| 404 | No such resource in your workspace. | +| 409 | Conflict, e.g. an MCP server slug already used in the workspace. | +| 422 | A remote spec could not be fetched or parsed (\`POST /api/connectors/{id}/import-spec\`). | +| 429 | Rate limit reached; see above. | + +Some import and discovery routes (\`POST /api/connectors/{id}/import\` and \`/discover-tools\`, and \`/import-spec\` for a connector without a spec) answer with a success status (201) and \`{"error": "…"}\` when the spec or the remote server cannot be read: check the body for an \`error\` field. + +## More + +- API guide: ${OPENAPI_API_DOCS_URL} +- CLI guide: ${OPENAPI_CLI_DOCS_URL} +- Source, issues and self-hosting: ${OPENAPI_REPO_URL} +`.trim(); + +/** The DocumentBuilder output for SwaggerModule.createDocument. */ +export function buildOpenApiConfig( + version: string = APP_VERSION, +): Omit { + const builder = new DocumentBuilder() + .setTitle('AnythingMCP API') + .setDescription(OPENAPI_DESCRIPTION) + .setVersion(version) + .setContact('AnythingMCP', 'https://anythingmcp.com', 'info@helpcode.ai') + .setLicense('AGPL-3.0', `${OPENAPI_REPO_URL}/blob/main/LICENSE`) + .setExternalDoc('AnythingMCP API guide', OPENAPI_API_DOCS_URL) + // Default scheme name 'bearer', referenced by @ApiBearerAuth(). + .addBearerAuth({ + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT or mcp_ key', + description: + 'A management-scoped API key (`mcp_…`, scope MANAGEMENT_READ or MANAGEMENT_ADMIN) ' + + 'or a dashboard session JWT. Paste the value without the "Bearer " prefix.', + }) + .addApiKey( + { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + description: + 'A management-scoped API key (`mcp_…`, scope MANAGEMENT_READ or MANAGEMENT_ADMIN), ' + + 'as an alternative to the bearer header. Accepted only on routes that also list this scheme.', + }, + API_KEY_SCHEME, + ); + for (const tag of OPENAPI_TAGS) builder.addTag(tag.name, tag.description); + return builder.build(); +} diff --git a/packages/backend/src/connectors/connectors.controller.ts b/packages/backend/src/connectors/connectors.controller.ts index 01955da57..166d997d8 100644 --- a/packages/backend/src/connectors/connectors.controller.ts +++ b/packages/backend/src/connectors/connectors.controller.ts @@ -19,7 +19,7 @@ import { Optional, } from '@nestjs/common'; import type { Response } from 'express'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiProperty, ApiPropertyOptional, ApiSecurity, ApiBody, ApiResponse } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { PaginationQueryDto } from '../common/pagination.dto'; import { ConfigService } from '@nestjs/config'; @@ -722,6 +722,7 @@ function mayExportSecrets(req: any): boolean { @ApiTags('Connectors') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/connectors') export class ConnectorsController { @@ -778,9 +779,18 @@ export class ConnectorsController { } @Get() - @ApiOperation({ summary: 'List connectors for current organization' }) - @ApiQuery({ name: 'limit', required: false, type: Number, description: '1..200' }) - @ApiQuery({ name: 'offset', required: false, type: Number }) + @ApiOperation({ + summary: 'List connectors for current organization', + description: + 'Every connector of your workspace, newest first, with its tools. Secrets are never returned: secret variables and ' + + 'headers come back empty and are named in `maskedEnvVars` / `maskedHeaders`, and `authConfig` is left out. ' + + '`setupStatus` says whether the connector can serve calls: `ready`, `needs_input` (the variables in ' + + '`missingVariables` are empty) or `needs_authorization` (a sign-in at the provider is pending). ' + + 'Without `limit` all connectors are returned.\n\n' + + '```bash\namcp api GET "/api/connectors?limit=50"\n```', + }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Page size, 1..200. Omit for all.', example: 50 }) + @ApiQuery({ name: 'offset', required: false, type: Number, description: 'Rows to skip.', example: 0 }) async list(@Req() req: any, @Query() pagination: PaginationQueryDto) { const rows = await this.connectorsService.findByOrg(req.user.organizationId, { limit: pagination.limit, @@ -827,7 +837,37 @@ export class ConnectorsController { } @Post() - @ApiOperation({ summary: 'Create a new connector' }) + @ApiOperation({ + summary: 'Create a new connector', + description: + 'Creates a hand-built connector. For an app in the catalogue, `POST /api/adapters/{slug}/import` is simpler: it ' + + 'creates the connector with its tools in one call. A new connector has no tools: add them with ' + + '`POST /api/connectors/{id}/import` (from a spec, Postman, cURL, HAR or JSON), `POST /api/connectors/{id}/import-spec` ' + + '(from `specUrl`), `POST /api/connectors/{id}/discover-tools` (MCP connectors) or the Tools endpoints.\n\n' + + 'A key typed into `baseUrl` or a header is moved into an encrypted variable and referenced as `{{VAR}}`. ' + + 'The connector is added to your default MCP server. File Search connectors are created with ' + + '`POST /api/file-search/connectors` instead. 403 for VIEWERs or when the plan allows no more connectors.', + }) + @ApiBody({ + type: CreateConnectorDto, + examples: { + rest: { + summary: 'REST API with a bearer token', + value: { + name: 'Acme CRM', + type: 'REST', + baseUrl: 'https://api.acme.example/v1', + authType: 'BEARER_TOKEN', + authConfig: { token: 'sk_live_…' }, + specUrl: 'https://api.acme.example/v1/openapi.json', + }, + }, + mcp: { + summary: 'Remote MCP server', + value: { name: 'Docs MCP', type: 'MCP', baseUrl: 'https://mcp.example.com/mcp', authType: 'NONE' }, + }, + }, + }) async create(@Req() req: any, @Body() dto: CreateConnectorDto) { this.assertCanCreate(req); if ((dto.type as string) === 'FILE_SEARCH') { @@ -1198,7 +1238,24 @@ export class ConnectorsController { @ApiOperation({ summary: 'Test connectivity of all active connectors', description: - 'Runs a health check against all active connectors and returns their status.', + 'Runs a health check against all active connectors and returns their status. Each check is a real request to the ' + + 'provider (the connector\'s `healthcheckPath`, or its type\'s test). A connector whose credential the provider ' + + 'refused for good is reported as `needs_reauth` without being called again.', + }) + @ApiResponse({ + status: 200, + schema: { + type: 'object', + example: { + total: 2, + healthy: 1, + unhealthy: 1, + connectors: [ + { id: 'cmob60dvv00c01zrpeujwmnep', name: 'HubSpot', type: 'REST', status: 'healthy', message: 'Connection successful', latencyMs: 231 }, + { id: 'cmob61abc00c11zrpq3x8kdm2', name: 'Acme CRM', type: 'REST', status: 'unhealthy', message: 'Auth handshake reached the API, but the credentials were rejected. Update authConfig and retry.', latencyMs: 118 }, + ], + }, + }, }) async healthCheck(@Req() req: any) { const allConnectors = await this.connectorsService.findByOrg(req.user.organizationId); @@ -1283,9 +1340,11 @@ export class ConnectorsController { 'so it can be restored with import-all or import. authConfig is never ' + 'exported. Secret environment variables and headers are exported only ' + 'to admins; for every other role, and for a read-only (MANAGEMENT_READ) ' + - 'API key, they are left empty and named in `maskedEnvVars` / `maskedHeaders`.', + 'API key, they are left empty and named in `maskedEnvVars` / `maskedHeaders`. ' + + 'The `X-Secrets-Included` response header says which it was.\n\n' + + '```bash\ncurl -s "$AMCP_URL/api/connectors//export?format=yaml" -H "Authorization: Bearer $AMCP_API_KEY" -o acme.yaml\n```', }) - @ApiQuery({ name: 'format', required: false, enum: ['json', 'yaml'] }) + @ApiQuery({ name: 'format', required: false, enum: ['json', 'yaml'], description: 'File format (default json).', example: 'yaml' }) async exportOne( @Req() req: any, @Param('id') id: string, @@ -1782,7 +1841,22 @@ export class ConnectorsController { } @Post(':id/test') - @ApiOperation({ summary: 'Test connector connection' }) + @ApiOperation({ + summary: 'Test connector connection', + description: + 'Makes one request to the provider with the stored settings (the connector\'s `healthcheckPath` for REST) and ' + + 'reports the outcome; nothing is changed. `kind` classifies it: `ok`, `auth_failed` (401/403, check the ' + + 'credentials), `not_found` (reachable, but the path does not exist), `unreachable` (DNS, network, timeout or a ' + + 'blocked address), `unsupported` or `error`. A connector with per-user credentials is tested with your own sign-in. ' + + 'Only the connector owner or an admin may test it.', + }) + @ApiResponse({ + status: 201, + schema: { + type: 'object', + example: { ok: false, kind: 'auth_failed', httpStatus: 401, message: 'Auth handshake reached the API, but the credentials were rejected. Update authConfig and retry.' }, + }, + }) async test(@Req() req: any, @Param('id') id: string) { const connector = await this.connectorsService.findById(id); this.assertCanWrite(connector, req); @@ -1856,7 +1930,17 @@ export class ConnectorsController { summary: 'Discover and import tools from a remote MCP server', description: 'Connects to the remote MCP server using stored credentials, ' + - 'lists all available tools, and imports them as MCP tools.', + 'lists all available tools, and imports them as MCP tools.\n\n' + + 'Run it again to pick up tools the remote server added or changed: existing tools are updated, new ones created, ' + + 'and tools the server no longer lists are deprecated (disabled, listed in `deprecated`). MCP connectors only. On failure the answer is ' + + 'a success status (201) with `{"error": "…"}`.', + }) + @ApiResponse({ + status: 201, + schema: { + type: 'object', + example: { message: 'Imported tools: created 12, updated 0', created: 12, updated: 0, deprecated: [], skipped: [], renameConflicts: [], tools: [{ id: '…', name: 'search_docs', isEnabled: true }] }, + }, }) async discoverMcpTools(@Req() req: any, @Param('id') id: string) { const connector = await this.connectorsService.findById(id); @@ -1949,7 +2033,15 @@ export class ConnectorsController { 'or YAML, and imports it like import-all: into the caller\'s ' + 'organization only, skipping connectors with duplicate names, never ' + 'restoring auth credentials. Not to be confused with POST :id/import, ' + - 'which imports tools from an API spec into an existing connector.', + 'which imports tools from an API spec into an existing connector.\n\n' + + '```bash\namcp api POST /api/connectors/import --data "$(jq -Rs \'{content: .}\' acme.yaml)" --yes\n```', + }) + @ApiResponse({ + status: 201, + schema: { + type: 'object', + example: { message: 'Imported 1 connectors with 14 tools', created: 1, skipped: 0, tools: 14, errors: [] }, + }, }) async importFile(@Req() req: any, @Body() dto: ImportFileDto) { this.assertCanCreate(req); @@ -2094,7 +2186,16 @@ export class ConnectorsController { } @Post(':id/import-spec') - @ApiOperation({ summary: 'Auto-generate MCP tools from API specification' }) + @ApiOperation({ + summary: 'Auto-generate MCP tools from API specification', + description: + 'Reads the spec the connector already points at and creates one tool per operation: the OpenAPI document at ' + + '`specUrl` (or the stored spec) for REST, the WSDL at `specUrl` or the base URL for SOAP, the schema by ' + + 'introspection for GraphQL. No body. Running it again updates the tools and deprecates (disables) those the spec no longer has. ' + + 'For other sources (Postman, cURL, HAR, JSON, inline content) use `POST /api/connectors/{id}/import`. ' + + 'A spec that cannot be fetched or parsed answers 422; a connector without a spec, or of a type with no spec import, ' + + 'answers a success status (201) with `{"error": "…"}`.', + }) async importSpec(@Req() req: any, @Param('id') id: string) { const connector = await this.connectorsService.findById(id); this.assertCanWrite(connector, req); @@ -2154,6 +2255,51 @@ export class ConnectorsController { @Post(':id/import') @ApiOperation({ summary: 'Import tools from any source: OpenAPI, Postman, cURL, HAR, WSDL, GraphQL', + description: + 'Parses `content` (inline text) or `url` with the parser named by `source` and creates or updates the ' + + 'connector\'s tools.\n\n' + + '- `openapi`, `postman`: `content` or `url`.\n' + + '- `wsdl`: `url` or `content` with the WSDL address, else the connector base URL.\n' + + '- `graphql`: introspects `url` or the connector base URL.\n' + + '- `curl`, `har`, `json`: `content` only. These **add** tools and deprecate nothing; `json` takes ' + + '`[{name, description, parameters, endpointMapping}]`.\n' + + '- `mcp`: lists the tools of the remote MCP server (MCP connectors; `url` is an optional MCP path).\n' + + '- `odata`: `service` path (and optional `entitySets`); its `$metadata` is fetched unless `content` holds it.\n\n' + + 'A full spec import (OpenAPI, WSDL, GraphQL, Postman, MCP) deprecates (disables) the tools the spec no longer has; ' + + 'an OData import only those of the same service. Tools are matched by operationId, then method and path, then name. ' + + 'Parse failures answer a success status (201) with `{"error": "…"}`.', + }) + @ApiBody({ + type: ImportToolsDto, + examples: { + openapiUrl: { + summary: 'OpenAPI from a URL', + value: { source: 'openapi', url: 'https://api.acme.example/v1/openapi.json' }, + }, + curl: { + summary: 'One tool from a cURL command', + value: { source: 'curl', content: "curl -X GET 'https://api.acme.example/v1/customers?limit=10' -H 'Accept: application/json'" }, + }, + odata: { + summary: 'SAP OData service', + value: { source: 'odata', service: '/sap/opu/odata/sap/API_BUSINESS_PARTNER', entitySets: ['A_BusinessPartner'] }, + }, + }, + }) + @ApiResponse({ + status: 201, + schema: { + type: 'object', + example: { + message: 'Imported tools: created 42, updated 3, deprecated 1', + created: 42, + updated: 3, + deprecated: ['get_legacy_report'], + skipped: [], + renameConflicts: [], + tools: [{ id: '…', name: 'list_customers', isEnabled: true }], + }, + }, }) async importTools( @Req() req: any, diff --git a/packages/backend/src/connectors/tools.controller.ts b/packages/backend/src/connectors/tools.controller.ts index d8d98eacd..dd00a2475 100644 --- a/packages/backend/src/connectors/tools.controller.ts +++ b/packages/backend/src/connectors/tools.controller.ts @@ -13,7 +13,7 @@ import { BadRequestException, ConflictException, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiProperty, ApiPropertyOptional, ApiSecurity } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { IsString, @@ -304,6 +304,7 @@ class PreviewMappingDto { @ApiTags('Tools') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/connectors/:connectorId/tools') export class ToolsController { diff --git a/packages/backend/src/file-search/file-search.controller.ts b/packages/backend/src/file-search/file-search.controller.ts index d983adca0..7360ea631 100644 --- a/packages/backend/src/file-search/file-search.controller.ts +++ b/packages/backend/src/file-search/file-search.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query, Req, 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 { FileSearchService } from './file-search.service'; import { FileAccessRulesDto, @@ -22,6 +22,7 @@ import { */ @ApiTags('File Search') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/file-search') export class FileSearchController { diff --git a/packages/backend/src/file-search/file-share.controller.ts b/packages/backend/src/file-search/file-share.controller.ts index 8136978ea..0591ecd55 100644 --- a/packages/backend/src/file-search/file-share.controller.ts +++ b/packages/backend/src/file-search/file-share.controller.ts @@ -1,6 +1,6 @@ import { Controller, Delete, ForbiddenException, Get, Param, Post, Query, Req, Res, UseGuards } from '@nestjs/common'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; import { FileShareService } from './file-share.service'; import { FileShareConnectorService } from './file-share-connector.service'; @@ -19,6 +19,8 @@ export class FileShareController { private readonly connector: FileShareConnectorService, ) {} + @ApiBearerAuth() + @ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Get() @ApiOperation({ summary: 'List File Share links' }) @@ -41,6 +43,8 @@ export class FileShareController { return { ...page, limits, usage, connector }; } + @ApiBearerAuth() + @ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Delete(':id') @ApiOperation({ summary: 'Revoke a File Share link' }) @@ -48,6 +52,8 @@ export class FileShareController { return this.shares.revoke(req.user.organizationId, id, req.user); } + @ApiBearerAuth() + @ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Post('connector') @ApiOperation({ summary: 'Create the File Share connector (admin only)' }) @@ -68,6 +74,8 @@ export class FileShareController { return this.shares.issueDownload(token, null); } + @ApiBearerAuth() + @ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Post('s/:token/download-internal') @ApiOperation({ summary: 'Issue a download for an internal share (signed-in member)' }) @@ -93,6 +101,8 @@ export class FileShareController { file.stream.pipe(res); } + @ApiBearerAuth() + @ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Get('limits') @ApiOperation({ summary: 'Get File Share limits and usage' }) diff --git a/packages/backend/src/identity-providers/identity-providers.controller.ts b/packages/backend/src/identity-providers/identity-providers.controller.ts index 6ce706259..2b9fa8dcc 100644 --- a/packages/backend/src/identity-providers/identity-providers.controller.ts +++ b/packages/backend/src/identity-providers/identity-providers.controller.ts @@ -18,6 +18,7 @@ import { ApiOperation, ApiProperty, ApiPropertyOptional, + ApiSecurity, } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { @@ -202,6 +203,7 @@ class ReplaceRoleMappingsDto { */ @ApiTags('Identity Providers') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(SelfHostedOnlyGuard, JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @Controller('api/identity-providers') diff --git a/packages/backend/src/knowledge-folders/knowledge-folders.controller.ts b/packages/backend/src/knowledge-folders/knowledge-folders.controller.ts index c8e05f623..fb7a07076 100644 --- a/packages/backend/src/knowledge-folders/knowledge-folders.controller.ts +++ b/packages/backend/src/knowledge-folders/knowledge-folders.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Req, UseGuards } from '@nestjs/common'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; -import { ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags, ApiSecurity } from '@nestjs/swagger'; import { ArrayMaxSize, ArrayNotEmpty, @@ -78,6 +78,7 @@ class MoveItemsDto { @ApiTags('Knowledge folders') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Controller('api/knowledge-folders') export class KnowledgeFoldersController { diff --git a/packages/backend/src/knowledge-graph/kg.controller.ts b/packages/backend/src/knowledge-graph/kg.controller.ts index f0bd14902..6587bfa5c 100644 --- a/packages/backend/src/knowledge-graph/kg.controller.ts +++ b/packages/backend/src/knowledge-graph/kg.controller.ts @@ -12,7 +12,7 @@ import { UseGuards, } from '@nestjs/common'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; -import { ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags, ApiSecurity } from '@nestjs/swagger'; import { ArrayMaxSize, ArrayNotEmpty, @@ -192,6 +192,7 @@ class SetMyLearningDto { @ApiTags('Knowledge Graph') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Controller('api/knowledge-graph') export class KgController { diff --git a/packages/backend/src/license/license.controller.ts b/packages/backend/src/license/license.controller.ts index 7a928e61a..4e0aca316 100644 --- a/packages/backend/src/license/license.controller.ts +++ b/packages/backend/src/license/license.controller.ts @@ -15,7 +15,7 @@ import { Logger, Optional, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiSecurity } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { Throttle } from '@nestjs/throttler'; import { IsBoolean, IsIn, IsString, IsOptional, Matches } from 'class-validator'; @@ -205,6 +205,7 @@ export class LicenseController { @Roles('ADMIN') @Throttle({ default: { limit: 5, ttl: 60_000 } }) @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Re-verify the workspace licence now, e.g. after returning from the billing portal (cloud, ADMIN)', @@ -260,6 +261,7 @@ export class LicenseController { @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Set and activate a license key (ADMIN)' }) async setLicenseKey(@Req() req: any, @Body() dto: SetLicenseKeyDto) { try { @@ -274,6 +276,7 @@ export class LicenseController { @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Open the Stripe billing portal for the current subscription (ADMIN)', }) @@ -296,6 +299,7 @@ export class LicenseController { // Each call creates a session on the licence site. @Throttle({ default: { limit: 10, ttl: 60_000 } }) @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'One-hour link to accept the Data Processing Agreement on the licence site (cloud, ADMIN, paid plans)', }) @@ -318,6 +322,7 @@ export class LicenseController { // this a few times at most, a loop should not be able to do more. @Throttle({ default: { limit: 10, ttl: 60_000 } }) @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Stripe Checkout link for a cloud plan, optionally as a card trial ending with the free trial (cloud, ADMIN)', @@ -404,6 +409,7 @@ export class LicenseController { @Get('pricing-link-token') @UseGuards(JwtOrManagementKeyGuard) @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: "Cloud: the signed-in user's address token for links to the pricing page (#t=), null when EMAIL_LINK_SECRET is unset", @@ -422,6 +428,7 @@ export class LicenseController { @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Cloud, ADMIN: whether this address can start a card trial (kind trial), must pay today because it had a subscription (pay), or neither applies (none)', @@ -442,6 +449,7 @@ export class LicenseController { @Get('card-trial-offer') @UseGuards(JwtOrManagementKeyGuard) @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Cloud: the card-trial offer after the first successful tool call, for admins of an eligible workspace (null otherwise)', @@ -459,6 +467,7 @@ export class LicenseController { @Get('wall') @UseGuards(JwtOrManagementKeyGuard) @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Cloud: the ended-trial licence wall for an admin: its variant and the workspace\'s own numbers (null otherwise)', @@ -480,6 +489,7 @@ export class LicenseController { @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Force re-verify license against remote API (ADMIN)' }) async verifyLicense(@Req() req: any) { const result = await this.licenseService.verifyLicense(undefined, req.user.organizationId); @@ -489,6 +499,7 @@ export class LicenseController { @Post('activate-trial') @UseGuards(JwtOrManagementKeyGuard) @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Start a 7-day cloud trial (cloud mode only)' }) async activateTrial(@Req() req: any) { if (!this.deployment.isCloud()) { @@ -542,6 +553,7 @@ export class LicenseController { @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @ApiBearerAuth() + @ApiSecurity('api-key') @ApiOperation({ summary: 'Request a free community license (sent via email)' }) async registerCommunity(@Req() req: any) { const user = await this.usersService.findById(req.user.sub); diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts index eb2ffda8f..686ec3079 100644 --- a/packages/backend/src/main.ts +++ b/packages/backend/src/main.ts @@ -20,7 +20,8 @@ startTracing(); import { NestFactory } from '@nestjs/core'; import { APP_VERSION, APP_COMMIT, APP_BUILD_DATE } from './common/app-version'; import { Logger, ValidationPipe } from '@nestjs/common'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { SwaggerModule } from '@nestjs/swagger'; +import { buildOpenApiConfig } from './common/openapi'; import { ConfigService } from '@nestjs/config'; import { Logger as PinoLogger } from 'nestjs-pino'; import cookieParser from 'cookie-parser'; @@ -132,26 +133,8 @@ async function bootstrap() { exposedHeaders: ['Content-Disposition', 'X-Secrets-Included'], }); - // Swagger documentation - const swaggerConfig = new DocumentBuilder() - .setTitle('AnythingMCP API') - .setDescription( - 'Backend API for AnythingMCP — convert any API into an MCP server. ' + - 'Manage connectors, configure MCP tools, and monitor usage.', - ) - .setVersion('0.1.0') - .addBearerAuth() - .addApiKey( - { type: 'apiKey', name: 'X-API-Key', in: 'header' }, - 'api-key', - ) - .addTag('Auth', 'Authentication and user management') - .addTag('Connectors', 'Manage API connectors') - .addTag('Tools', 'MCP tool configuration') - .addTag('AI', 'AI-assisted configuration') - .addTag('MCP', 'MCP server management') - .addTag('Health', 'Health checks') - .build(); + // Swagger documentation (metadata, tags and auth schemes: common/openapi.ts) + const swaggerConfig = buildOpenApiConfig(APP_VERSION); const document = SwaggerModule.createDocument(app, swaggerConfig); SwaggerModule.setup('api/docs', app, document, { diff --git a/packages/backend/src/mcp-server/mcp-endpoint.controller.ts b/packages/backend/src/mcp-server/mcp-endpoint.controller.ts index 49e542e5c..cebb8e881 100644 --- a/packages/backend/src/mcp-server/mcp-endpoint.controller.ts +++ b/packages/backend/src/mcp-server/mcp-endpoint.controller.ts @@ -109,6 +109,7 @@ import { planServerResources, } from './mcp-resources'; import { flattenToolSchema, jsonSchemaToZodShape, stripEnvVarParams } from './tool-schema.util'; +import { ApiTags } from '@nestjs/swagger'; function trimSlash(url: string | undefined): string { return (url || '').trim().replace(/\/+$/, ''); @@ -269,6 +270,7 @@ const SKILL_INSTRUCTION_MAX = 1800; * each MCP server its own unique URL that clients like Claude Desktop * can connect to independently (via OAuth or API key). */ +@ApiTags('MCP Endpoint') @Controller('mcp') @SkipThrottle() // Auth first, then the per-caller ceiling (it needs to know who is calling). diff --git a/packages/backend/src/mcp-server/well-known-oauth.controller.ts b/packages/backend/src/mcp-server/well-known-oauth.controller.ts index c84e00c6d..e1a85151c 100644 --- a/packages/backend/src/mcp-server/well-known-oauth.controller.ts +++ b/packages/backend/src/mcp-server/well-known-oauth.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Param, Req, Res } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { ApiTags } from '@nestjs/swagger'; import type { Request, Response } from 'express'; /** @@ -24,6 +25,7 @@ import type { Request, Response } from 'express'; * resource is always `/mcp/`) rather than a wildcard, to avoid any * path-matching ambiguity. */ +@ApiTags('OAuth discovery') @Controller('.well-known') export class WellKnownOAuthController { constructor(private readonly config: ConfigService) {} diff --git a/packages/backend/src/mcp-servers/mcp-connections.controller.ts b/packages/backend/src/mcp-servers/mcp-connections.controller.ts index 273c1f6ff..c7d349471 100644 --- a/packages/backend/src/mcp-servers/mcp-connections.controller.ts +++ b/packages/backend/src/mcp-servers/mcp-connections.controller.ts @@ -9,7 +9,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 { McpConnectionGrantService } from './mcp-connection-grant.service'; import { PrismaService } from '../common/prisma.service'; import { @@ -37,10 +37,11 @@ interface UpdateConnectionDto { * Changes take effect on the connection's very next request: the grant is * re-read, and re-validated, per request. */ -@ApiTags('mcp-connections') +@ApiTags('MCP Connections') @Controller('api/mcp-connections') @UseGuards(JwtOrManagementKeyGuard) @ApiBearerAuth() +@ApiSecurity('api-key') export class McpConnectionsController { constructor( private readonly grants: McpConnectionGrantService, diff --git a/packages/backend/src/mcp-servers/mcp-servers.controller.ts b/packages/backend/src/mcp-servers/mcp-servers.controller.ts index eb818c892..1abb45c66 100644 --- a/packages/backend/src/mcp-servers/mcp-servers.controller.ts +++ b/packages/backend/src/mcp-servers/mcp-servers.controller.ts @@ -12,7 +12,7 @@ import { NotFoundException, ForbiddenException, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery, ApiProperty, ApiPropertyOptional, ApiSecurity, ApiBody, ApiResponse } from '@nestjs/swagger'; import { PaginationQueryDto } from '../common/pagination.dto'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { IsString, IsOptional, IsBoolean, IsArray, IsIn } from 'class-validator'; @@ -89,6 +89,7 @@ class UpdateMcpServerDto { enum: CODE_MODE_MODES, description: 'Code mode for this server: off (default, no code execution), tools (saved code tools only), inline (also the generic code_execute / code_propose_tool). Needs a configured code-runner to actually run.', + example: 'inline', }) @IsOptional() @IsIn(CODE_MODE_MODES as unknown as string[]) @@ -109,6 +110,7 @@ class AssignConnectorsDto { @ApiTags('MCP Servers') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/mcp-servers') export class McpServersController { @@ -135,9 +137,16 @@ export class McpServersController { } @Get() - @ApiOperation({ summary: 'List MCP servers for current organization' }) - @ApiQuery({ name: 'limit', required: false, type: Number, description: '1..200' }) - @ApiQuery({ name: 'offset', required: false, type: Number }) + @ApiOperation({ + summary: 'List MCP servers for current organization', + description: + 'Every MCP server of your workspace, oldest first. Each one is an MCP endpoint at `/mcp/{id}` that exposes the ' + + 'tools of its connectors. Includes the names of its connectors, `_count` (connectors, API keys) and `usage` ' + + '(`calls30d`, `lastCallAt`). Without `limit` all servers are returned.\n\n' + + '```bash\namcp api GET /api/mcp-servers\n```', + }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Page size, 1..200. Omit for all.', example: 50 }) + @ApiQuery({ name: 'offset', required: false, type: Number, description: 'Rows to skip.', example: 0 }) async list(@Req() req: any, @Query() pagination: PaginationQueryDto) { const servers = await this.mcpServersService.findAllByOrg(req.user.organizationId, { limit: pagination.limit, @@ -154,7 +163,28 @@ export class McpServersController { } @Post() - @ApiOperation({ summary: 'Create a new MCP server' }) + @ApiOperation({ + summary: 'Create a new MCP server', + description: + 'Creates an empty MCP server; its endpoint is `/mcp/{id}` (the `id` in the response). Then put connectors on it ' + + 'with `PUT /api/mcp-servers/{id}/connectors`, set code mode or skills delivery with `PUT /api/mcp-servers/{id}`, ' + + 'and give AI clients a key with `POST /api/mcp-keys` (`mcpServerId`) or let them sign in with OAuth. ' + + 'A slug is derived from the name when omitted; a slug already used in the workspace answers 409. ' + + '403 when the plan allows no more MCP servers.', + }) + @ApiBody({ + type: CreateMcpServerDto, + examples: { + sales: { + summary: 'A server for the sales team', + value: { + name: 'Sales', + description: 'CRM and e-mail tools for the sales team', + instructions: 'Use HubSpot for customer data. Never delete records.', + }, + }, + }, + }) async create(@Req() req: any, @Body() dto: CreateMcpServerDto) { await this.licenseGuard.checkCanCreateMcpServer(req.user.sub, req.user.organizationId); return this.mcpServersService.create(req.user.sub, req.user.organizationId, dto); @@ -176,8 +206,14 @@ export class McpServersController { @Get(':id/activity') @ApiOperation({ - summary: - 'Calls in the last 30 days and the time of the last one. Polled by the connect page to show when the first request from an AI client arrives.', + summary: 'Calls in the last 30 days and the time of the last one', + description: + 'Tool calls made through this server in the last 30 days and when the last one happened. Polled by the connect ' + + 'page to show when the first request from an AI client arrives; also a cheap way to check a server is in use.', + }) + @ApiResponse({ + status: 200, + schema: { type: 'object', example: { calls30d: 1284, lastCallAt: '2026-11-05T09:41:12.000Z' } }, }) async activity(@Req() req: any, @Param('id') id: string) { const server = await this.mcpServersService.findById(id); @@ -188,7 +224,30 @@ export class McpServersController { } @Put(':id') - @ApiOperation({ summary: 'Update MCP server' }) + @ApiOperation({ + summary: 'Update MCP server', + description: + 'Changes only the fields sent. `codeMode` controls code execution on this server:\n\n' + + '- `off` (default): no code execution.\n' + + '- `tools`: saved code tools (approved programs stored as tools) can run.\n' + + '- `inline`: also the generic `code_execute` (run a short program that calls several tools in one step) and ' + + '`code_propose_tool` (propose a program as a saved code tool for an admin to approve).\n\n' + + 'Code runs in an isolated sandbox and needs a configured code-runner. `skillsDelivery` sets how applied skills ' + + 'reach clients (`auto`, `inline`, `index`); `isActive: false` switches the endpoint off. Only the server owner or ' + + 'an admin may change it; a slug already used in the workspace answers 409.\n\n' + + '```bash\namcp api PUT /api/mcp-servers/ --data \'{"codeMode":"inline"}\' --yes\n```', + }) + @ApiBody({ + type: UpdateMcpServerDto, + examples: { + codeMode: { summary: 'Turn on code mode', value: { codeMode: 'inline' } }, + disable: { summary: 'Switch the endpoint off', value: { isActive: false } }, + instructions: { + summary: 'Change the instructions sent to AI clients', + value: { instructions: 'Answer in English. Use the ERP for prices, never the web.' }, + }, + }, + }) async update(@Req() req: any, @Param('id') id: string, @Body() dto: UpdateMcpServerDto) { const server = await this.mcpServersService.findById(id); if (!server) throw new NotFoundException('MCP server not found'); @@ -207,7 +266,16 @@ export class McpServersController { } @Put(':id/connectors') - @ApiOperation({ summary: 'Assign connectors to MCP server' }) + @ApiOperation({ + summary: 'Assign connectors to MCP server', + description: + 'Sets the connectors whose tools this server exposes. The list **replaces** the current assignment: send every ' + + 'connector that should stay, or `[]` to remove them all. Connected AI clients are told the tool list changed. ' + + '404 when an id is not a connector of your workspace. Which tools each user may call is then decided by roles ' + + '(`/api/roles`).\n\n' + + '```bash\namcp api PUT /api/mcp-servers//connectors --data \'{"connectorIds":[""]}\' --yes\n```', + }) + @ApiResponse({ status: 200, schema: { type: 'object', example: { message: 'Connectors assigned' } } }) async assignConnectors( @Req() req: any, @Param('id') id: string, diff --git a/packages/backend/src/memory/memory.controller.ts b/packages/backend/src/memory/memory.controller.ts index cc221e19b..44d845b80 100644 --- a/packages/backend/src/memory/memory.controller.ts +++ b/packages/backend/src/memory/memory.controller.ts @@ -13,7 +13,7 @@ import { UseGuards, } from '@nestjs/common'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; -import { ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiTags, ApiSecurity } from '@nestjs/swagger'; import { ArrayMaxSize, ArrayNotEmpty, @@ -139,6 +139,7 @@ class SetMemoryAccessDto { @ApiTags('Memory') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Controller('api/memory') export class MemoryController { diff --git a/packages/backend/src/organizations/organizations.controller.ts b/packages/backend/src/organizations/organizations.controller.ts index 6bda5fc9a..d626c0051 100644 --- a/packages/backend/src/organizations/organizations.controller.ts +++ b/packages/backend/src/organizations/organizations.controller.ts @@ -13,7 +13,7 @@ import { NotFoundException, BadRequestException, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiSecurity } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { IsString, IsOptional, IsBoolean } from 'class-validator'; import { OrganizationsService } from './organizations.service'; @@ -77,6 +77,7 @@ class DeleteOrgDto { @ApiTags('Organizations') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/organizations') export class OrganizationsController { diff --git a/packages/backend/src/roles/mcp-api-keys.controller.ts b/packages/backend/src/roles/mcp-api-keys.controller.ts index 8d3cbefe9..f9b32d6cf 100644 --- a/packages/backend/src/roles/mcp-api-keys.controller.ts +++ b/packages/backend/src/roles/mcp-api-keys.controller.ts @@ -9,7 +9,7 @@ import { UseGuards, } from '@nestjs/common'; import { ForbiddenException } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiSecurity, ApiBody, ApiResponse } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { IsString, IsOptional, IsIn } from 'class-validator'; import { McpApiKeysService } from './mcp-api-keys.service'; @@ -45,13 +45,19 @@ class CreateKeyDto { @ApiTags('MCP API Keys') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/mcp-keys') export class McpApiKeysController { constructor(private readonly keysService: McpApiKeysService) {} @Get() - @ApiOperation({ summary: 'List my MCP API keys' }) + @ApiOperation({ + summary: 'List my MCP API keys', + description: + 'Your own keys in every scope, with name, scope, server, active flag and last use. The key value is masked ' + + '(only its last 8 characters are shown); the full value is returned once, at creation.', + }) async listKeys(@Req() req: any) { const keys = await this.keysService.listByUser(req.user.sub); // Mask keys for display: show only last 8 chars @@ -62,7 +68,44 @@ export class McpApiKeysController { } @Post() - @ApiOperation({ summary: 'Generate a new MCP API key' }) + @ApiOperation({ + summary: 'Generate a new MCP API key', + description: + 'Creates a key `mcp_…` owned by you and returns it in full, **once**: store it now.\n\n' + + '- `scope: "MCP"` (default): for AI clients calling tools on `/mcp` (`X-API-Key` or `Authorization: Bearer`). ' + + 'With `mcpServerId` the key works on that server only.\n' + + '- `scope: "MANAGEMENT_READ"`: this API, `GET`/`HEAD` only, e.g. for dashboards and audits.\n' + + '- `scope: "MANAGEMENT_ADMIN"`: this API, any method. Only admins can create one (403 otherwise).\n\n' + + 'A key always acts with its owner\'s role and never grants more. `amcp login --browser` creates a management key ' + + 'for the CLI the same way. Revoke with `POST /api/mcp-keys/{id}/revoke`.', + }) + @ApiBody({ + type: CreateKeyDto, + examples: { + client: { + summary: 'Key for an AI client on one server', + value: { name: 'claude-desktop', mcpServerId: 'cmo9x1k2a0001abcdpq7rs8tu', scope: 'MCP' }, + }, + ciRead: { summary: 'Read-only key for CI', value: { name: 'ci-audit', scope: 'MANAGEMENT_READ' } }, + admin: { summary: 'Full management key (admins only)', value: { name: 'terraform', scope: 'MANAGEMENT_ADMIN' } }, + }, + }) + @ApiResponse({ + status: 201, + description: 'The new key. `key` is shown only in this response.', + schema: { + type: 'object', + example: { + id: 'cmoa2b3c40002efghuv9wx0yz', + key: 'mcp_3f9a…c41d', + name: 'ci-audit', + isActive: true, + mcpServerId: null, + scope: 'MANAGEMENT_READ', + createdAt: '2026-11-05T10:00:00.000Z', + }, + }, + }) async generateKey(@Req() req: any, @Body() dto: CreateKeyDto) { const scope: McpApiKeyScope = dto.scope ?? 'MCP'; // A full-access management key is only meaningful for an admin; cap the @@ -82,7 +125,12 @@ export class McpApiKeysController { } @Post(':id/revoke') - @ApiOperation({ summary: 'Revoke (deactivate) an MCP API key' }) + @ApiOperation({ + summary: 'Revoke (deactivate) an MCP API key', + description: + 'Deactivates one of your own keys: from the next request on it is refused (401), on `/mcp` and on this API. ' + + 'The key stays in your list as inactive; `DELETE /api/mcp-keys/{id}` removes it. Only your own keys are affected.', + }) async revokeKey(@Req() req: any, @Param('id') id: string) { await this.keysService.revoke(id, req.user.sub); return { message: 'Key revoked' }; diff --git a/packages/backend/src/roles/roles.controller.ts b/packages/backend/src/roles/roles.controller.ts index e78d83331..0bfd27fc2 100644 --- a/packages/backend/src/roles/roles.controller.ts +++ b/packages/backend/src/roles/roles.controller.ts @@ -11,7 +11,7 @@ import { ForbiddenException, NotFoundException, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiSecurity, ApiBody, ApiResponse } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { IsString, IsOptional, IsArray, ArrayMaxSize } from 'class-validator'; import { Roles, RolesGuard } from '../auth/roles.guard'; @@ -77,6 +77,7 @@ class SetUserRolesDto { @ApiTags('Roles') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @Controller('api/roles') @@ -84,13 +85,30 @@ export class RolesController { constructor(private readonly rolesService: RolesService) {} @Get() - @ApiOperation({ summary: 'List all roles (ADMIN)' }) + @ApiOperation({ + summary: 'List all roles (ADMIN)', + description: + 'The MCP roles of your workspace plus the built-in system roles. An MCP role is a whitelist of tools: a member ' + + 'with one or more roles may call only the tools those roles allow (the union); a member with no MCP role keeps ' + + 'access to every tool of the servers they reach. Separate from the workspace role (ADMIN, EDITOR, VIEWER).', + }) async listRoles(@Req() req: any) { return this.rolesService.findAll(req.user.organizationId); } @Post() - @ApiOperation({ summary: 'Create a custom role (ADMIN)' }) + @ApiOperation({ + summary: 'Create a custom role (ADMIN)', + description: + 'Creates an empty role. Next, choose its tools with `PUT /api/roles/{id}/tools` and give it to members with ' + + '`PUT /api/roles/assignments/{userId}`.', + }) + @ApiBody({ + type: CreateRoleDto, + examples: { + sales: { summary: 'Read-only sales role', value: { name: 'sales-read-only', description: 'CRM lookups, no writes' } }, + }, + }) async createRole(@Req() req: any, @Body() dto: CreateRoleDto) { return this.rolesService.create({ ...dto, organizationId: req.user.organizationId }); } @@ -140,7 +158,24 @@ export class RolesController { } @Put(':id/tools') - @ApiOperation({ summary: 'Set tool access for a role (ADMIN)' }) + @ApiOperation({ + summary: 'Set tool access for a role (ADMIN)', + description: + 'Sets the tools members with this role may call. The list **replaces** the current one; `[]` allows none. ' + + 'Tool ids are the `id` of each tool in `GET /api/connectors` (its `tools`) or `GET /api/connectors/{id}/tools`, ' + + 'and must all belong to your workspace. System roles cannot be changed.\n\n' + + '```bash\namcp api PUT /api/roles//tools --data \'{"toolIds":["",""]}\' --yes\n```', + }) + @ApiBody({ + type: SetToolAccessDto, + examples: { + twoTools: { + summary: 'Allow two tools', + value: { toolIds: ['cmob7a1b200d01zrp5h6j7k8l', 'cmob7a1b200d11zrp9m0n1p2q'] }, + }, + }, + }) + @ApiResponse({ status: 200, schema: { type: 'object', example: { message: 'Tool access updated' } } }) async setToolAccess( @Req() req: any, @Param('id') id: string, @@ -174,7 +209,22 @@ export class RolesController { } @Get('assignments/:userId') - @ApiOperation({ summary: 'List the MCP roles assigned to a user (ADMIN)' }) + @ApiOperation({ + summary: 'List the MCP roles assigned to a user (ADMIN)', + description: + 'The MCP roles a member holds, with where each assignment comes from (`source`: `manual`, or set by identity ' + + 'provider group sync). User ids are in `GET /api/users`.', + }) + @ApiResponse({ + status: 200, + schema: { + type: 'object', + example: { + roleIds: ['cmoc1d2e300e01zrpr3s4t5u6'], + roles: [{ id: 'cmoc1d2e300e01zrpr3s4t5u6', name: 'sales-read-only', source: 'manual' }], + }, + }, + }) async getAssignments(@Req() req: any, @Param('userId') userId: string) { const assignments = await this.rolesService.getUserRoles( userId, @@ -191,7 +241,24 @@ export class RolesController { } @Put('assignments/:userId') - @ApiOperation({ summary: 'Set the MCP roles assigned to a user (ADMIN)' }) + @ApiOperation({ + summary: 'Set the MCP roles assigned to a user (ADMIN)', + description: + 'Replaces the member\'s **manual** role assignments with `roleIds` (at most 100; `[]` clears them). Assignments ' + + 'that come from identity provider group sync are kept. The member may then call the union of the tools of all ' + + 'their roles. 404 when the user is not a member of your workspace or a role id is unknown.', + }) + @ApiBody({ + type: SetUserRolesDto, + examples: { + one: { summary: 'Give one role', value: { roleIds: ['cmoc1d2e300e01zrpr3s4t5u6'] } }, + clear: { summary: 'Remove all manual roles', value: { roleIds: [] } }, + }, + }) + @ApiResponse({ + status: 200, + schema: { type: 'object', example: { message: 'Roles updated', roleIds: ['cmoc1d2e300e01zrpr3s4t5u6'] } }, + }) async setAssignments( @Req() req: any, @Param('userId') userId: string, diff --git a/packages/backend/src/settings/site-settings.controller.ts b/packages/backend/src/settings/site-settings.controller.ts index 0f7c4fad3..606333d57 100644 --- a/packages/backend/src/settings/site-settings.controller.ts +++ b/packages/backend/src/settings/site-settings.controller.ts @@ -9,7 +9,7 @@ import { UseGuards, BadRequestException, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiSecurity } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { IsString, IsNumber, IsOptional, IsBoolean, IsArray, ValidateNested, Matches } from 'class-validator'; import { Type } from 'class-transformer'; @@ -120,6 +120,7 @@ export class SiteSettingsPublicController { */ @ApiTags('Site Settings') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/settings') export class SiteSettingsMemberController { @@ -140,6 +141,7 @@ export class SiteSettingsMemberController { @ApiTags('Site Settings') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard, RolesGuard) @Roles('ADMIN') @Controller('api/admin/settings') diff --git a/packages/backend/src/users/users.controller.ts b/packages/backend/src/users/users.controller.ts index 9442dbf23..0c1290bd2 100644 --- a/packages/backend/src/users/users.controller.ts +++ b/packages/backend/src/users/users.controller.ts @@ -16,7 +16,7 @@ import { NotFoundException, ForbiddenException, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiProperty, ApiPropertyOptional, ApiSecurity, ApiResponse } from '@nestjs/swagger'; import { JwtOrManagementKeyGuard } from '../auth/jwt-or-management-key.guard'; import { IsString, IsOptional, IsEmail, IsEnum, IsBoolean, MinLength, Matches, Equals } from 'class-validator'; import { UserRole } from '../generated/prisma/client'; @@ -117,6 +117,7 @@ class DeleteSelfDto { @ApiTags('Users') @ApiBearerAuth() +@ApiSecurity('api-key') @UseGuards(JwtOrManagementKeyGuard) @Controller('api/users') export class UsersController { @@ -128,7 +129,29 @@ export class UsersController { ) {} @Get('me') - @ApiOperation({ summary: 'Get current user profile' }) + @ApiOperation({ + summary: 'Get current user profile', + description: + 'The account behind the credential: with an API key, the key\'s owner. A quick way to check that a credential ' + + 'works; `amcp login` calls it to validate a key. `organizationId` is the workspace last active in the dashboard. ' + + 'The password hash is never returned.\n\n' + + '```bash\ncurl -s "$AMCP_URL/api/users/me" -H "Authorization: Bearer $AMCP_API_KEY"\n```', + }) + @ApiResponse({ + status: 200, + schema: { + type: 'object', + example: { + id: 'cmo8a7b6c0000abcdx1y2z3w4', + email: 'ana@example.com', + name: 'Ana', + role: 'ADMIN', + emailVerified: true, + organizationId: 'cmo8a7b6c0001abcdk5l6m7n8', + createdAt: '2026-03-02T08:15:00.000Z', + }, + }, + }) async getProfile(@Req() req: any) { const user = await this.usersService.findById(req.user.sub); if (!user) return { error: 'User not found' }; @@ -278,7 +301,34 @@ export class UsersController { @Get() @UseGuards(RolesGuard) @Roles('ADMIN') - @ApiOperation({ summary: 'List all users in organization (ADMIN only)' }) + @ApiOperation({ + summary: 'List all users in organization (ADMIN only)', + description: + 'The members of your workspace, in the order they joined, with their workspace `role` (ADMIN, EDITOR, VIEWER), ' + + 'whether they are `active` (not deactivated), their legacy single MCP role and their memory access. Use the `id` ' + + 'with `/api/roles/assignments/{userId}` to manage MCP roles.', + }) + @ApiResponse({ + status: 200, + schema: { + type: 'array', + items: { type: 'object' }, + example: [ + { + id: 'cmo8a7b6c0000abcdx1y2z3w4', + email: 'ana@example.com', + name: 'Ana', + role: 'ADMIN', + organizationId: 'cmo8a7b6c0001abcdk5l6m7n8', + mcpRoleId: null, + mcpRole: null, + joinedAt: '2026-03-02T08:15:00.000Z', + deactivatedAt: null, + active: true, + }, + ], + }, + }) async listUsers(@Req() req: any) { return this.usersService.findAll(req.user.organizationId); } diff --git a/packages/cli/README.md b/packages/cli/README.md index 304820c53..47431f4b7 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,151 +1,379 @@ -# AnythingMCP CLI +

+ + amcp, the AnythingMCP CLI: log in through the browser, list and call MCP tools such as shopify_search_orders, pipe the JSON into jq, and test a connector through the management API. + +

-The `amcp` command connects to one AnythingMCP MCP server from your terminal. -It supports login, logout, profile inspection, tool discovery and tool calls. +

AnythingMCP CLI (amcp)

-## About AnythingMCP +

+ Call MCP tools, run code-mode programs and manage your whole MCP gateway from the terminal.
+ The official command-line client for AnythingMCP, the open-source agent harness and MCP gateway. +

-[AnythingMCP](https://anythingmcp.com) is an enterprise **agent harness and MCP -gateway**: it turns any API, SaaS or data source into [MCP](https://modelcontextprotocol.io) -tools your AI agents can use — Claude, ChatGPT, Cursor and others. Connect -services like Odoo, Etsy, Shopify and HubSpot, SQL and NoSQL databases, and -SFTP/FTP or SMB file shares, then govern who and which agent may reach each one. -Run it on AnythingMCP Cloud or self-host it (AGPL-3.0). Source and docs: -[github.com/HelpCode-ai/anythingmcp](https://github.com/HelpCode-ai/anythingmcp). +

+ npm version + npm downloads + Node.js version + License: AGPL-3.0 +

-This package is the command-line client. Beyond calling MCP tools, it drives the -management API with a management-scoped key, so a person — or a connected agent — -can configure the whole instance (connectors, MCP servers, users, roles, SSO, -settings and more) without leaving the terminal. +

+ Website · + Guide with examples · + API reference · + AnythingMCP on GitHub +

-From the repository root: +--- + +`amcp` does two jobs with one binary: + +- **Calls MCP tools.** These are the same tools your AI clients use (Shopify, HubSpot, SAP, Odoo, Postgres, an SFTP share…). You can run them by hand, in shell scripts, from cron and CI, or as a **code-mode program** that loops over dozens of tool calls in a single step. +- **Drives the management API.** With a management-scoped key, everything the dashboard does — connectors, MCP servers, users, roles, SSO, memory, skills, audit — is one `amcp api` command away, for a person or for a connected AI agent. + +It works with [AnythingMCP Cloud](https://cloud.anythingmcp.com) and self-hosted instances. ```bash -npm install -npm run build -w packages/cli -node packages/cli/dist/index.js login --url http://localhost:4000 --server YOUR_SERVER_ID -node packages/cli/dist/index.js tools ls +npm i -g @anythingmcp/cli ``` -`login` prompts for an MCP API key without displaying it. For scripts, use -`--key-stdin` or the `AMCP_API_KEY` environment variable. Never put the key in -a command argument. The CLI verifies the key by requesting `tools/list` before -saving it. It always connects to `/mcp/`, including on Cloud, because -the bare `/mcp` endpoint exposes a different tool set there. Pass the instance -base URL to `--url`, without `/mcp/`. HTTPS is required except for -`localhost`, `127.0.0.0/8`, and `::1` during local development. Redirects are -refused so the API key cannot follow a request to another origin. +Requires Node.js 22.12 or newer. The examples below use real commands, flags, endpoints and tool names (the Shopify, HubSpot and Slack tools come from the built-in catalogue connectors). Only the data in the output is invented. + +## Contents + +- [Quick start](#quick-start) +- [Recipes](#recipes) + - [Call a tool with real arguments](#call-a-tool-with-real-arguments) + - [Pipe results into jq, cron and CI](#pipe-results-into-jq-cron-and-ci) + - [Code mode: one program, many tool calls](#code-mode-one-program-many-tool-calls) + - [Stand up a workspace in one script](#stand-up-a-workspace-in-one-script) + - [Turn your own API into MCP tools](#turn-your-own-api-into-mcp-tools) + - [See what your agents did](#see-what-your-agents-did) +- [Command reference](#command-reference) +- [Management API (`amcp api`)](#management-api-amcp-api) +- [Shell completion](#shell-completion) +- [Security](#security) +- [About AnythingMCP](#about-anythingmcp) + +## Quick start + +`amcp` uses two kinds of key, each in its own profile: + +| You want to… | Key | Log in with | +|---|---|---| +| Call the tools of one MCP server | an **MCP key** for that server | `amcp login --url … --server SERVER_ID` | +| Manage the instance (`amcp api`) | a **management key** (read-only or admin) | `amcp login --browser` | + +```console +$ amcp login --url https://cloud.anythingmcp.com --server cmob60dvv00c01zrpeujwmnep +MCP API key: +Logged in to https://cloud.anythingmcp.com (profile: default). + +$ amcp tools ls --filter order +shopify_search_orders read-only Search orders with Shopify search syntax (for example `financial_status:paid … +shopify_get_order read-only Get one order: status, totals (subtotal, shipping, tax, discounts, refunds) … + +$ amcp login --browser --scope admin --url https://cloud.anythingmcp.com +Logged in to https://cloud.anythingmcp.com (management, profile: default). + +$ amcp api GET /api/mcp-servers | jq -r '.[] | "\(.id) \(.name)"' +cmob60dvv00c01zrpeujwmnep Default +cmoc2k4hd00f11zrp9xq3vbnw Sales +``` + +## Recipes + +### Call a tool with real arguments + +`tools describe` prints a tool's full description and input JSON schema. `tools call` runs the tool with arguments that match it: + +```console +$ amcp tools describe shopify_search_orders +$ amcp tools call shopify_search_orders \ + --args '{"query":"financial_status:paid created_at:>=2026-11-01","first":2}' +{ + "orders": { + "pageInfo": { "hasNextPage": true, "endCursor": "eyJsYXN0X2lkIjo2…" }, + "nodes": [ + { + "name": "#1043", + "displayFinancialStatus": "PAID", + "displayFulfillmentStatus": "UNFULFILLED", + "currentTotalPriceSet": { "shopMoney": { "amount": "412.80", "currencyCode": "EUR" } }, + "customer": { "displayName": "Kellerhaus GmbH" } + }, + … +``` + +Arguments can also come from a file (`--args @args.json`) or from stdin (`--args -`). + +### Pipe results into jq, cron and CI + +A tool's text output is its JSON on stdout. Errors go to stderr with a distinct exit code, so `amcp` composes like any Unix tool: + +```console +$ amcp tools call shopify_search_orders \ + --args '{"query":"fulfillment_status:unfulfilled created_at:<2026-11-03"}' \ + | jq -r '.orders.nodes[] | [.name, .customer.displayName, .createdAt[:10]] | @tsv' +#1038 Kellerhaus GmbH 2026-10-30 +#1035 Studio Lumen 2026-10-29 +#1031 Bäckerei Hofmann 2026-10-28 +``` + +A daily job that posts late orders to Slack (the Slack connector sits on the same MCP server): ```bash -printf '%s\n' "$AMCP_API_KEY" | amcp login --url https://cloud.anythingmcp.com \ - --server YOUR_SERVER_ID --key-stdin --profile cloud -amcp tools ls --profile cloud --filter customer -amcp whoami --profile cloud -amcp logout --profile cloud +#!/usr/bin/env bash +# late-orders.sh — run from cron: 0 8 * * 1-5 +set -euo pipefail +SINCE=$(date -d '-2 days' +%F) # GNU date; on macOS: date -v-2d +%F +LATE=$(amcp tools call shopify_search_orders \ + --args "{\"query\":\"fulfillment_status:unfulfilled created_at:<$SINCE\"}" \ + | jq -r '.orders.nodes[] | "• \(.name) — \(.customer.displayName)"') +[ -z "$LATE" ] && exit 0 +jq -n --arg text "Orders waiting more than 2 days: +$LATE" '{channel: "#fulfilment", text: $text}' \ + | amcp tools call slack_post_message --args - --yes +``` + +### Code mode: one program, many tool calls + +

+ A code-mode program that pages through every paid Shopify order since October, keeps the five biggest customers and looks each one up in HubSpot, run from the terminal with a single amcp tools call code_execute. +

+ +When an MCP server's `codeMode` is `inline`, it offers a `code_execute` tool. You send it a short JavaScript program, and a sandbox next to your tools runs it with three helpers: `amcp.search(query)`, `amcp.describe(tool)` and `amcp.run(tool, args)`. + +The question here: *who are our five biggest Shopify customers since October, and where is each one in HubSpot?* + +```js +// top-customers.js — runs inside the AnythingMCP sandbox: read-only, your own permissions +const byEmail = new Map(); +let after = null, pages = 0; + +// 1. Page through every paid Shopify order since October +do { + const r = await amcp.run("shopify_search_orders", { + query: "financial_status:paid created_at:>=2026-10-01", + first: 250, after, + }); + for (const o of r.orders.nodes) { + if (!o.email) continue; + const c = byEmail.get(o.email) + ?? { name: o.customer?.displayName, orders: 0, revenue: 0 }; + c.orders += 1; + c.revenue += Number(o.currentTotalPriceSet.shopMoney.amount); + byEmail.set(o.email, c); + } + const { hasNextPage, endCursor } = r.orders.pageInfo; + after = hasNextPage ? endCursor : null; + pages++; +} while (after); + +// 2. Keep the five biggest customers +const top = [...byEmail] + .sort((a, b) => b[1].revenue - a[1].revenue) + .slice(0, 5); + +// 3. Join each one with its HubSpot contact +const result = []; +for (const [email, c] of top) { + const h = await amcp.run("hubspot_search_objects", { + object_type: "contacts", + filter_groups: [{ filters: [ + { propertyName: "email", operator: "EQ", value: email }, + ] }], + properties: ["lifecyclestage"], + limit: 1, + }); + const stage = h.results[0]?.properties.lifecyclestage ?? "not in CRM"; + result.push({ email, ...c, revenue: c.revenue.toFixed(2), stage }); +} +return { pagesScanned: pages, top: result }; +``` + +```console +$ amcp api PUT /api/mcp-servers/$SRV --yes --data '{"codeMode":"inline"}' # once, admin key +$ jq -Rs '{code: .}' top-customers.js | amcp tools call code_execute --args - +{ + "pagesScanned": 3, + "top": [ + { + "email": "einkauf@kellerhaus.de", + "name": "Kellerhaus GmbH", + "orders": 14, + "revenue": "4820.50", + "stage": "customer" + }, + … + ] +} ``` -For CI, `AMCP_URL`, `AMCP_SERVER` and `AMCP_API_KEY` can provide connection -details without saving a profile. `--url` and `--server` take precedence over -their matching environment variables. If a URL is supplied by `--url` or -`AMCP_URL`, supply `AMCP_API_KEY` too; a saved profile key is used only with its -saved origin. Profiles live in -`$XDG_CONFIG_HOME/amcp/config.json` or `~/.config/amcp/config.json`, with file -mode `0600` on Unix. Relative `XDG_CONFIG_HOME` values are ignored. `whoami` -shows only a key prefix. One profile can hold both an MCP key (for `tools`) and -a management key (for `api`): logging in with one keeps the other when both -point at the same instance. `amcp --version` prints the CLI version. +That's **8 tool calls** (3 pages of orders and 5 CRM lookups) for **one** call from your terminal. You get back five customers instead of 750 raw orders, which also matters when the caller is an AI agent: it reads one short answer, so it spends fewer tokens and makes fewer mistakes. -`tools ls` shows each tool's name, read-only/destructive hint, and the first -line of its description. `tools describe` shows the full description and input -JSON schema. Use that schema to prepare arguments for `tools call`: +The guardrails: + +- Programs can only call **read-only** tools, with the permissions of the calling key. +- They run in an **isolated sandbox**: no filesystem, network or environment access, a deadline, and a cap on tool calls per run. +- Every inner call is **audited**. +- Inline code needs the server's `codeMode` set to `inline` *and* a role with the inline-code capability. It's available on the Team plan and above. + +When a program is worth keeping, `code_propose_tool` records it as a proposal for an admin. Once approved, it becomes a named tool for every AI client on the server: + +```console +$ jq -Rs '{ + name: "top_customers_with_crm_stage", + description: "Top 5 Shopify customers since October, with their HubSpot lifecycle stage", + rationale: "Sales asks for this every Monday", + code: . + }' top-customers.js | amcp tools call code_propose_tool --args - +{"ok":true,"proposalId":"cmh4k2…","message":"Proposal recorded for admin review. It will not run or become a tool until an admin approves it."} +``` + +### Stand up a workspace in one script + +Install a catalogue connector with its token, give the sales team its own MCP server, attach the connector, turn on code mode and test the connection: ```bash -amcp tools describe YOUR_TOOL_NAME -amcp tools call YOUR_TOOL_NAME --args '{"id":"123"}' -amcp tools call YOUR_TOOL_NAME --args @args.json -cat args.json | amcp tools call YOUR_TOOL_NAME --args - -``` - -Without `--args`, the CLI sends an empty object. Arguments must be a JSON -object. Calls time out after 300 seconds by default. Set `--timeout ` -on `tools call` for a different positive timeout (fractional seconds are allowed). -A timeout exits with code 4 and reports `Tool call timed out after s`. -For a tool annotated as destructive, an interactive terminal asks for -confirmation. In a non-interactive script, pass `--yes` explicitly. Tool text -goes to stdout, and non-text content is available with `--json`. - -`--json` works with every command. It writes one JSON object to stdout: login -and logout report the profile, `whoami` reports the instance URL, MCP `endpoint` -and masked key, `tools ls` returns `{ "tools": [...] }`, -`tools describe` returns `{ "tool": {...} }`, -and `tools call` returns `{ "tool": "name", "result": {...} }`. CLI usage and -connection errors go to stderr (also as JSON when `--json` is set). A tool's -`isError` result is returned on stdout in JSON mode and on stderr in text mode. -JSON errors on stderr have the shape `{ "error": "message", "exitCode": 4 }`. -For example: +#!/usr/bin/env bash +# bootstrap.sh — needs: amcp login --browser --scope admin +set -euo pipefail + +CONN=$(jq -n --arg t "$HUBSPOT_TOKEN" '{credentials: {HUBSPOT_ACCESS_TOKEN: $t}}' \ + | amcp api POST /api/adapters/hubspot/import --data - --yes | jq -r .connectorId) + +SRV=$(amcp api POST /api/mcp-servers --yes \ + --data '{"name":"Sales","description":"CRM tools for the sales team"}' | jq -r .id) + +amcp api PUT /api/mcp-servers/$SRV/connectors --yes --data "{\"connectorIds\":[\"$CONN\"]}" +amcp api PUT /api/mcp-servers/$SRV --yes --data '{"codeMode":"inline"}' +amcp api POST /api/connectors/$CONN/test --yes +# → { "ok": true, "kind": "ok", "message": "Connection successful" } +``` + +The token travels through `jq --arg` and stdin, so it never shows up in `ps` or your shell history. Every non-GET call in a script needs an explicit `--yes`. + +### Turn your own API into MCP tools + +Create a REST connector, then import its OpenAPI spec. WSDL, GraphQL, Postman, cURL and HAR work the same way, through `source`: ```bash -amcp tools ls --json -amcp tools call YOUR_TOOL_NAME --args @args.json --json +ID=$(amcp api POST /api/connectors --yes --data '{ + "name": "Acme Billing", "type": "REST", + "baseUrl": "https://api.acme.example/v1", + "authType": "BEARER_TOKEN", "authConfig": { "token": "'"$ACME_TOKEN"'" } +}' | jq -r .id) + +amcp api POST /api/connectors/$ID/import --yes \ + --data '{"source":"openapi","url":"https://api.acme.example/v1/openapi.json"}' ``` -Exit codes are `0` for success, `1` when a tool returns `isError`, `2` for -invalid usage or JSON arguments, `3` for authentication failures, and `4` for -connection or server failures. The API key is never accepted as an argument -and is never included in CLI status output. +### See what your agents did -## Shell completion +Every tool call, from people or agents, is audited: + +```console +$ amcp api GET "/api/audit/invocations?status=ERROR&limit=3" \ + | jq -r '.[] | [.createdAt[11:16], .tool.name, .user.email, .error[:40]] | @tsv' +08:41 hubspot_search_objects lea@acme.io HTTP 429: retry after 1s +07:02 shopify_get_order ops-agent@acme.io Order not found +06:55 zendesk_get_ticket marco@acme.io HTTP 404: RecordNotFound +``` + +`GET /api/audit/breakdowns?days=30` returns usage and cost by connector, MCP server and user. + +## Command reference + +| Command | What it does | +|---|---| +| `amcp login --url URL --server ID` | Save an MCP key for one server's tools (prompted, never echoed) | +| `amcp login --browser [--scope admin] --url URL` | Mint a management key through the dashboard | +| `amcp login --management --url URL --key-stdin` | Save an existing management key | +| `amcp whoami` / `amcp logout` | Show / remove the current profile | +| `amcp tools ls [--filter TEXT]` | List tools with their read-only/destructive hint | +| `amcp tools describe TOOL` | Full description and input JSON schema | +| `amcp tools call TOOL [--args JSON\|@file\|-] [--yes] [--timeout S]` | Call a tool | +| `amcp api METHOD PATH [--data JSON\|@file\|-] [--yes]` | Call the management API | +| `amcp api --list [RESOURCE]` | List management operations offline | +| `amcp completion bash\|zsh\|fish` | Print a shell completion script | +| `amcp --version` | Print the CLI version | + +Every command accepts `--profile NAME` and `--json`. -`amcp completion ` prints a completion script for commands, -`tools` subcommands and each command's flags. Install it once for your shell: +### Logging in with an MCP key + +`login` prompts for an MCP API key without displaying it. For scripts, use `--key-stdin` or the `AMCP_API_KEY` environment variable. Never put the key in a command argument. The CLI checks the key by requesting `tools/list` before saving it. + +It always connects to `/mcp/`, including on Cloud, because the bare `/mcp` endpoint exposes a different tool set there. Pass the instance base URL to `--url`, without `/mcp/`. HTTPS is required except for `localhost`, `127.0.0.0/8` and `::1` during local development. Redirects are refused, so the API key can't follow a request to another origin. ```bash -amcp completion bash > ~/.local/share/bash-completion/completions/amcp -amcp completion zsh > "${fpath[1]}/_amcp" -amcp completion fish > ~/.config/fish/completions/amcp.fish +printf '%s\n' "$AMCP_API_KEY" | amcp login --url https://cloud.anythingmcp.com \ + --server YOUR_SERVER_ID --key-stdin --profile cloud +amcp tools ls --profile cloud --filter customer +amcp whoami --profile cloud +amcp logout --profile cloud ``` -Bash needs the `bash-completion` package for the directory above; without it, -add `source <(amcp completion bash)` to `~/.bashrc`. In zsh, `compinit` must be -enabled; to load the script without writing a file, add -`source <(amcp completion zsh)` to `~/.zshrc` after `compinit`. Open a new shell -afterwards. The scripts are static and do not read the config file or contact a -server, so profile and tool names are not completed. `completion` always prints -the script as plain text, even with `--json`. Any shell other than `bash`, `zsh` -or `fish` exits with code `2`. +### Profiles and environment + +For CI, `AMCP_URL`, `AMCP_SERVER` and `AMCP_API_KEY` provide the connection without saving a profile. `--url` and `--server` take precedence over their matching environment variables. If a URL comes from `--url` or `AMCP_URL`, supply `AMCP_API_KEY` too: a saved profile key is used only with its saved origin. + +Profiles live in `$XDG_CONFIG_HOME/amcp/config.json` or `~/.config/amcp/config.json`, with file mode `0600` on Unix. Relative `XDG_CONFIG_HOME` values are ignored. `whoami` shows only a key prefix. + +One profile can hold both an MCP key (for `tools`) and a management key (for `api`). Logging in with one keeps the other when both point at the same instance. + +### Tool calls + +`tools ls` shows each tool's name, its read-only/destructive hint and the first line of its description. `tools describe` shows the full description and input JSON schema. + +- Without `--args`, the CLI sends an empty object. Arguments must be a JSON object. +- Calls time out after 300 seconds by default. Set `--timeout ` for a different positive timeout (fractional seconds are allowed). A timeout exits with code 4 and reports `Tool call timed out after s`. +- A tool annotated as destructive asks for confirmation in an interactive terminal. In a non-interactive script, pass `--yes` explicitly. +- Tool text goes to stdout. Non-text content is available with `--json`. + +### JSON output and exit codes + +`--json` works with every command. It writes one JSON object to stdout: + +| Command | JSON output | +|---|---| +| `login` / `logout` | the profile | +| `whoami` | the instance URL, MCP `endpoint` and masked key | +| `tools ls` | `{ "tools": [...] }` | +| `tools describe` | `{ "tool": {...} }` | +| `tools call` | `{ "tool": "name", "result": {...} }` | +| `api` | `{ "status": 200, "data": ... }` | + +CLI usage and connection errors go to stderr, also as JSON when `--json` is set, with the shape `{ "error": "message", "exitCode": 4 }`. A tool's `isError` result goes to stdout in JSON mode and to stderr in text mode. + +| Exit code | Meaning | +|---|---| +| `0` | success | +| `1` | the tool returned `isError`, or the API answered 4xx | +| `2` | invalid usage or JSON arguments | +| `3` | authentication failed (401/403) | +| `4` | connection failure, server error (5xx) or timeout | ## Management API (`amcp api`) -Beyond calling MCP tools, `amcp` can drive the instance's **management API** -(`/api/*`) — connectors, MCP servers, roles, users, SSO, settings, memory, -skills, the knowledge graph and more — with a **management-scoped** API key. +`amcp api` calls the instance's management API (`/api/*`) with a **management-scoped** key. That covers connectors, MCP servers, roles, users, SSO, settings, memory, skills, the knowledge graph and more. It works like `gh api`, but for AnythingMCP. -A management key is an ordinary MCP API key minted with a management scope -(`MANAGEMENT_READ` or `MANAGEMENT_ADMIN`) from the dashboard or `POST -/api/mcp-keys`. It carries the owner's identity and role: it never grants -privilege, so admin-only routes still require an admin owner. `MANAGEMENT_READ` -keys may call only safe methods (GET/HEAD), and connector exports made with -them never include secrets, even for an admin owner. +A management key is an ordinary MCP API key minted with a management scope: `MANAGEMENT_READ` or `MANAGEMENT_ADMIN`. You can mint one from the dashboard or with `POST /api/mcp-keys`. The key carries its owner's identity and role, and it never grants privilege, so admin-only routes still need an admin owner. `MANAGEMENT_READ` keys can only call safe methods (GET/HEAD), and connector exports made with them never include secrets, even for an admin owner. ### Getting a management key -Mint one from the dashboard (Settings → Management API keys), choosing -read-only or admin, or let the CLI do it through the browser: +Mint one from the dashboard (Settings → Management API keys), choosing read-only or admin, or let the CLI do it through the browser: ```bash amcp login --browser --url https://cloud.anythingmcp.com # read-only amcp login --browser --scope admin --url https://cloud.anythingmcp.com ``` -`--browser` opens the instance's authorization page, where you approve the -request under your existing dashboard session (SSO included). The CLI prints a -short verification code that must match the page before you approve. The key is -minted by the dashboard and returned to the CLI over a one-shot `127.0.0.1` -loopback, so it never appears in a URL, log or shell history; it is an ordinary -management key you can see and revoke in Settings. On a headless machine, set -`AMCP_NO_BROWSER=1` and open the printed URL yourself. The admin scope is -available only to workspace admins. This is additive: the key-based `login ---management` below is unchanged. +`--browser` opens the instance's authorization page, where you approve the request under your existing dashboard session (SSO included). The CLI prints a short verification code; check that it matches the page before you approve. + +The dashboard mints the key and returns it to the CLI over a one-shot `127.0.0.1` loopback, so it never appears in a URL, log or shell history. It's an ordinary management key that you can see and revoke in Settings. On a headless machine, set `AMCP_NO_BROWSER=1` and open the printed URL yourself. The admin scope is available only to workspace admins. ### Using a management key @@ -156,7 +384,7 @@ printf '%s\n' "$AMCP_API_KEY" | amcp login --management --url https://cloud.anyt amcp api GET /api/connectors amcp api GET /api/mcp-servers amcp api POST /api/mcp-servers --data '{"name":"ops","description":"Ops server"}' -amcp api PUT /api/mcp-servers/SERVER_ID --data '{"codeMode":"tools"}' +amcp api PUT /api/mcp-servers/SERVER_ID --data '{"codeMode":"inline"}' ``` For CI, skip the profile and pass the key in the environment: @@ -165,26 +393,75 @@ For CI, skip the profile and pass the key in the environment: AMCP_URL=https://cloud.anythingmcp.com AMCP_API_KEY=mcp_… amcp api GET /api/users --json ``` -To discover what the management API offers, list its operations (no key and no -network — the catalogue ships with the CLI): +The key comes from `AMCP_API_KEY`, `--key-stdin` or a saved management profile, never from a command argument. + +- A non-safe method asks for confirmation in a terminal. Pass `--yes` in scripts. +- `--data` accepts inline JSON, `@file.json` or `-` (stdin). +- Output is the parsed JSON body. `--json` wraps it with the status. +- Exit codes match the rest of the CLI: `1` for a 4xx result, `3` for 401/403, `4` for 5xx or a connection failure. +- The management API applies a per-key rate limit, so a runaway client gets HTTP 429 instead of overloading the instance. + +### Discovering operations + +The catalogue of management operations ships with the CLI, so listing it needs no key and no network: + +```console +$ amcp api --list mcp-servers +mcp-servers + GET /api/mcp-servers — List MCP servers for current organization + POST /api/mcp-servers [body] — Create a new MCP server + DELETE /api/mcp-servers/:id — Delete MCP server + GET /api/mcp-servers/:id — Get MCP server detail + PUT /api/mcp-servers/:id [body] — Update MCP server + … +$ amcp api --list --json # { "operations": [ { method, path, summary, admin, … } ] } +``` + +Each line shows the method, the path, an `[admin]` tag when the route needs an admin owner, a `[body]` tag when it takes a request body, and the summary. The catalogue is generated from the backend controllers, and CI fails if a management route is added without regenerating it, so `amcp api --list` always matches the instance's API. The interactive reference with request examples is in the instance's Swagger UI at `/api/docs`. + +## Shell completion + +`amcp completion ` prints a completion script for commands, `tools` subcommands and each command's flags. Install it once for your shell: ```bash -amcp api --list # every management operation, grouped by resource -amcp api --list mcp-servers # only one resource -amcp api --list --json # { "operations": [ { method, path, summary, admin, … } ] } -``` - -Each line shows the method, path, an `[admin]` tag when the route needs an admin -owner, a `[body]` tag when it takes a request body, and the summary. The -catalogue is generated from the backend controllers, and CI fails if a new -management route is added without regenerating it, so `amcp api --list` always -matches the instance's API. - -The key comes from `AMCP_API_KEY`, `--key-stdin`, or a saved management profile — -never from a command argument. A non-safe method asks for confirmation on a -terminal; pass `--yes` in scripts. `--data` accepts inline JSON, `@file.json` or -`-` (stdin). Output is the parsed JSON body; `--json` wraps it with the status. -Exit codes match the rest of the CLI: `1` for a 4xx result, `3` for 401/403, -`4` for 5xx or a connection failure. The management API applies a per-key -rate limit, so a runaway client is refused with HTTP 429 rather than hammering -the instance. +amcp completion bash > ~/.local/share/bash-completion/completions/amcp +amcp completion zsh > "${fpath[1]}/_amcp" +amcp completion fish > ~/.config/fish/completions/amcp.fish +``` + +- **Bash** needs the `bash-completion` package for the directory above. Without it, add `source <(amcp completion bash)` to `~/.bashrc`. +- **Zsh** needs `compinit` enabled. To load the script without writing a file, add `source <(amcp completion zsh)` to `~/.zshrc` after `compinit`. + +Open a new shell afterwards. The scripts are static: they don't read the config file or contact a server, so profile and tool names aren't completed. `completion` always prints the script as plain text, even with `--json`. Any shell other than `bash`, `zsh` or `fish` exits with code `2`. + +## Security + +- The API key is never accepted as a command argument and never printed in status output. +- Keys are stored with file mode `0600`. +- HTTPS is required except on loopback. +- Redirects are refused, so a key can't follow a request to another origin. +- Writes in scripts need an explicit `--yes`. +- Management keys are capped by their scope and by their owner's role, rate-limited per key, and audited. + +## About AnythingMCP + +[AnythingMCP](https://anythingmcp.com) is an open-source, enterprise **agent harness and MCP gateway**. It turns any API, SaaS or data source into [Model Context Protocol](https://modelcontextprotocol.io) tools your AI agents can use: Claude, ChatGPT, Copilot, Cursor, Gemini and others. + +You can connect hundreds of catalogue services (Odoo, SAP, Shopify, HubSpot, Etsy and more), SQL and NoSQL databases, and SFTP/FTP or SMB file shares. You can also import your own OpenAPI, WSDL, GraphQL or Postman definitions. Then you decide which people and which agents may reach each tool, with roles, SSO and a full audit log. + +Run it on [AnythingMCP Cloud](https://cloud.anythingmcp.com) or self-host it. Source and docs: [github.com/HelpCode-ai/anythingmcp](https://github.com/HelpCode-ai/anythingmcp). + +### Developing the CLI + +From the repository root: + +```bash +npm install +npm run build -w packages/cli +node packages/cli/dist/index.js --help +npm test -w packages/cli +``` + +## License + +[AGPL-3.0-only](https://github.com/HelpCode-ai/anythingmcp/blob/main/LICENSE) diff --git a/packages/cli/assets/code-mode.html b/packages/cli/assets/code-mode.html new file mode 100644 index 000000000..44dcb6d63 --- /dev/null +++ b/packages/cli/assets/code-mode.html @@ -0,0 +1,99 @@ + + + + +
+
+
+

Code mode

+

One program, many tool calls.

+

8 calls in the sandbox · 1 call from your terminal · 5 customers back, not 750 orders

+
+
+
top-customers.jsthe program
// Runs inside the AnythingMCP sandbox: read-only, your own permissions.
+const byEmail = new Map();
+let after = null, pages = 0;
+
+// 1. Page through every paid Shopify order since October
+do {
+  const r = await amcp.run("shopify_search_orders", {
+    query: "financial_status:paid created_at:>=2026-10-01",
+    first: 250, after,
+  });
+  for (const o of r.orders.nodes) {
+    if (!o.email) continue;
+    const c = byEmail.get(o.email) ?? { name: o.customer?.displayName, orders: 0, revenue: 0 };
+    c.orders += 1;
+    c.revenue += Number(o.currentTotalPriceSet.shopMoney.amount);
+    byEmail.set(o.email, c);
+  }
+  const { hasNextPage, endCursor } = r.orders.pageInfo;
+  after = hasNextPage ? endCursor : null;
+  pages++;
+} while (after);
+
+// 2. Keep the five biggest customers, then join each with HubSpot
+const top = [...byEmail].sort((a, b) => b[1].revenue - a[1].revenue).slice(0, 5);
+const result = [];
+for (const [email, c] of top) {
+  const h = await amcp.run("hubspot_search_objects", {
+    object_type: "contacts", properties: ["lifecyclestage"], limit: 1,
+    filter_groups: [{ filters: [{ propertyName: "email", operator: "EQ", value: email }] }],
+  });
+  const stage = h.results[0]?.properties.lifecyclestage ?? "not in CRM";
+  result.push({ email, ...c, revenue: c.revenue.toFixed(2), stage });
+}
+return { pagesScanned: pages, top: result };
+
zshrun it from the CLI
$ jq -Rs '{code: .}' top-customers.js \
+  | amcp tools call code_execute --args -
+{
+  "pagesScanned": 3,
+  "top": [
+    {
+      "email": "einkauf@kellerhaus.de",
+      "name": "Kellerhaus GmbH",
+      "orders": 14,
+      "revenue": "4820.50",
+      "stage": "customer"
+    },
+    {
+      "email": "anna@studio-lumen.it",
+      "name": "Studio Lumen",
+      "orders": 9,
+      "revenue": "3110.00",
+      "stage": "opportunity"
+    },
+    …
+  ]
+}
+
+
diff --git a/packages/cli/assets/code-mode.png b/packages/cli/assets/code-mode.png new file mode 100644 index 000000000..2abced523 Binary files /dev/null and b/packages/cli/assets/code-mode.png differ diff --git a/packages/cli/assets/hero.html b/packages/cli/assets/hero.html new file mode 100644 index 000000000..a2bf160c0 --- /dev/null +++ b/packages/cli/assets/hero.html @@ -0,0 +1,56 @@ + + + + +
+
+
+

AnythingMCP · CLI

+

amcp
from the terminal.

+

Call MCP tools, run code-mode programs and manage your whole instance — by hand, in scripts or from an agent.

+

$npm i -g @anythingmcp/cli

+
+
zsh — ~/work
+
$ amcp login --browser --url https://cloud.anythingmcp.com
+Logged in to https://cloud.anythingmcp.com (management, profile: default).
+$ amcp tools ls --filter order
+shopify_search_orders   read-only   Search orders with Shopify search syntax…
+shopify_get_order       read-only   Get one order: status, totals, items…
+$ amcp tools call shopify_search_orders \
+    --args '{"query":"fulfillment_status:unfulfilled"}' \
+  | jq -r '.orders.nodes[] | [.name, .customer.displayName] | @tsv'
+#1038   Kellerhaus GmbH
+#1035   Studio Lumen
+#1031   Bäckerei Hofmann
+$ amcp api POST /api/connectors/$CONN/test --yes
+{ "ok": true, "kind": "ok", "message": "Connection successful" }
+
diff --git a/packages/cli/assets/hero.png b/packages/cli/assets/hero.png new file mode 100644 index 000000000..2e7734869 Binary files /dev/null and b/packages/cli/assets/hero.png differ diff --git a/packages/cli/package.json b/packages/cli/package.json index 801278c3c..1f5946c4c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@anythingmcp/cli", "version": "0.1.4", - "description": "Command-line client for AnythingMCP, the enterprise agent harness and MCP gateway: call MCP tools and fully manage your instance — connectors, MCP servers, users, roles and settings — from the terminal.", + "description": "Command-line client for AnythingMCP, the open-source agent harness and MCP gateway: call MCP tools from your shell, scripts and CI, run code-mode programs that chain many tool calls in one step, and manage your instance — connectors, MCP servers, users, roles, audit — from the terminal.", "license": "AGPL-3.0-only", "keywords": [ "anythingmcp", @@ -18,7 +18,15 @@ "claude", "anthropic", "chatgpt", - "cursor" + "cursor", + "code-mode", + "code-execution", + "mcp-tools", + "openapi", + "api-gateway", + "automation", + "devops", + "terminal" ], "homepage": "https://github.com/HelpCode-ai/anythingmcp/tree/main/packages/cli#readme", "repository": { diff --git a/packages/cli/src/generated/operations.ts b/packages/cli/src/generated/operations.ts index df5c6f5e9..beecff5d2 100644 --- a/packages/cli/src/generated/operations.ts +++ b/packages/cli/src/generated/operations.ts @@ -1807,7 +1807,7 @@ export const OPERATIONS: readonly ApiOperation[] = [ "action": "activity", "method": "GET", "path": "/api/mcp-servers/:id/activity", - "summary": "Calls in the last 30 days and the time of the last one. Polled by the connect page to show when the first request from an AI client arrives.", + "summary": "Calls in the last 30 days and the time of the last one", "admin": false, "pathParams": [ "id"