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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 107 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,32 +45,128 @@ aem-mcp
```

### Configuration

```
Options:
--version Show version number [boolean]
-H, --host [string] [default: "http://localhost:4502"]
-u, --user [string] [default: "admin"]
-p, --pass [string] [default: "admin"]
-i, --id clientId [string] [default: ""]
-s, --secret clientSecret [string] [default: ""]
-m, --mcpPort [number] [default: 8502]
-h, --help Show help [boolean]
-H, --host [string] [default: "http://localhost:4502"]
-u, --user [string] [default: "admin"]
-p, --pass [string] [default: "admin"]
-i, --id clientId [string] [default: ""]
-s, --secret clientSecret [string] [default: ""]
-C, --cert path to client certificate PEM file for mTLS to AEM. [string]
-k, --key path to private key PEM file for mTLS to AEM. [string]
--ca path to CA bundle PEM file (self-signed AEM tenants). [string]
--cert-watch-interval-min poll cert mtime every N minutes; reload on change. 0 disables (default).
[number] [default: 0]
-m, --mcpPort [number] [default: 8502]
--bind host interface to bind. Default 127.0.0.1 (loopback only).
[string] [default: "127.0.0.1"]
--shutdown-drain-seconds max seconds to wait for in-flight requests on SIGINT/SIGTERM.
[number] [default: 60]
--allow-origin extra Origin header value to allow on /mcp (repeatable).
[array] [default: []]
-h, --help Show help [boolean]
```

For AEMaaCS, use the `clientId` and `clientSecret` for authentication. [More info](https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication/implementation).
For self-hosted AEM use user/pass. The default credentials are `admin:admin`.
### Authentication modes

The server supports three auth modes for talking to AEM. The factory picks
the strongest one available, with cert-auth taking priority over OAuth and
OAuth over Basic:

| Mode | Flags | When to use |
|---|---|---|
| **Basic** | `-u/-p` (defaults `admin/admin`) | Local AEM, on-prem AEM where Basic is still enabled |
| **OAuth (Adobe IMS S2S)** | `-i <clientId> -s <clientSecret>` | AEMaaCS tenants. [More info](https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication/implementation). |
| **mTLS (client certificate)** | `--cert <path> --key <path> [--ca <path>]` | Air-gapped AEM, enterprise mTLS gateways, compliance-driven tenants (PCI-DSS, FedRAMP, HIPAA), org-issued per-developer client certs |

If you supply both `--cert/--key` and `--id/--secret`, cert-auth wins and a
warning is logged that OAuth params will be ignored.

> **Trust boundary** — mTLS authenticates the **server → AEM** leg only. It
> does **not** authenticate the **MCP client → server** leg. The `/mcp`
> endpoint stays open to anyone who can reach the bind interface; the
> server defaults to loopback (`127.0.0.1`) precisely because of this. If
> you change `--bind` to a non-loopback address, you must put a reverse
> proxy with auth in front yourself.

#### Encrypted private keys

If your private key is PKCS#8-encrypted (`-----BEGIN ENCRYPTED PRIVATE KEY-----`),
supply the passphrase via the `AEM_KEY_PASSPHRASE` env var. There is
intentionally **no `--passphrase` CLI flag** — CLI arguments are visible
to any user on the machine through `ps aux`, env vars are not. For org
PKIs that mandate encrypted-key export (Venafi, internal CAs), this is
the supported path.

#### Cert rotation (production deployments)

Production PKIs (cert-manager, HashiCorp Vault, ACM) rotate client certs
every 60–90 days. Two rotation paths are supported, both without a
process restart:

- **SIGHUP-driven** — replace the PEM files on disk, then
`kill -HUP <pid>`. The server re-reads the PEMs, validates the keypair,
and atomically swaps the cached `undici.Agent`. The previous Agent's
keep-alive sockets drain for 30 s before being destroyed. Bad PEM
material is rejected before the swap, so the old cert keeps serving.
- **mtime-driven** — start the server with `--cert-watch-interval-min N`
(env: `AEM_CERT_WATCH_INTERVAL_MIN`). Every N minutes the server polls
the cert file's mtime; on change, the same reload code path runs.
Off by default (`0`).

Both paths emit a stderr line like
`[cert-reload] strategy reloaded: SHA256(old)=<hash> → SHA256(new)=<hash>`
so SREs can correlate rotations in their logs.

#### Revocation (CRL / OCSP)

The server does **not** perform CRL or OCSP revocation checks of the
AEM server cert. This is a deliberate scope decision — handle
revocation at the upstream layer (Dispatcher, reverse proxy, mTLS
gateway) where you already have central PKI configuration. The TLS
handshake still validates the AEM server cert chain against the CA
bundle (`--ca` or the OS trust store).

#### Environment variables

| Variable | Purpose |
|---|---|
| `MCP_LOGGER` | Set to `true` to enable diagnostic logging on stdout (off by default — required off for MCP stdio clients). |
| `MCP_USERNAME` / `MCP_PASSWORD` | Optional HTTP Basic auth gate on `POST /mcp` (only active when both are set). |
| `MCP_BIND` | Default bind interface (overrides built-in `127.0.0.1`). CLI `--bind` takes precedence. |
| `MCP_ALLOWED_ORIGINS` | Comma-separated extra `Origin` values allowed on `/mcp`. Inspector ports 6274/6277 on `localhost`/`127.0.0.1` are always allowed. |
| `MCP_SHUTDOWN_DRAIN_SECONDS` | Default SIGINT/SIGTERM drain budget. CLI `--shutdown-drain-seconds` takes precedence. |
| `AEM_IMS_URL` | Override the Adobe IMS token endpoint. Defaults to `https://ims-na1.adobelogin.com/ims/token`. Set to `https://ims-eu1.adobelogin.com/ims/token` (EMEA) or `https://ims-jp1.adobelogin.com/ims/token` (APAC) for non-NA AEMaaCS tenants. |
| `AEM_CERT_PATH` / `AEM_KEY_PATH` / `AEM_CA_PATH` | Cert-auth paths (env-var alternatives to `--cert/--key/--ca`). |
| `AEM_KEY_PASSPHRASE` | Passphrase for an encrypted private key. **Env-only — no CLI equivalent.** |
| `AEM_CERT_WATCH_INTERVAL_MIN` | Default cert mtime poll interval in minutes. CLI `--cert-watch-interval-min` takes precedence. |

> **Production recommendation** — prefer env vars over CLI flags for
> credential paths. Env vars are visible only to the process owner (and
> root) via `/proc/<pid>/environ`; CLI args appear in `ps aux` for
> anyone on the host.

### Example Commands

### Example Command
```sh
# Basic auth (default — local AEM)
aem-mcp -u=user@domain.com -p=mypass -H=https://author-qa.domain.com

# OAuth (AEMaaCS)
aem-mcp -i=<clientId> -s=<clientSecret> -H=https://author-pXXX.adobeaemcloud.com

# mTLS with explicit CA bundle
aem-mcp --cert=/etc/aem-mcp/client.crt --key=/etc/aem-mcp/client.key \
--ca=/etc/aem-mcp/ca.crt \
-H=https://author.internal.example.com

# mTLS via env vars (production — keeps paths out of ps aux)
export AEM_CERT_PATH=/etc/aem-mcp/client.crt
export AEM_KEY_PATH=/etc/aem-mcp/client.key
export AEM_CA_PATH=/etc/aem-mcp/ca.crt
export AEM_KEY_PASSPHRASE='...' # only if the key is encrypted
aem-mcp -H=https://author.internal.example.com --cert-watch-interval-min=15
```

### Add AEM MCP to AI IDE
Expand Down
65 changes: 0 additions & 65 deletions docs/phase1-smoke-audit.md

This file was deleted.

30 changes: 23 additions & 7 deletions package-lock.json

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

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
"@modelcontextprotocol/sdk": "^1.17.3",
"cors": "^2.8.5",
"express": "^5.1.0",
"yargs": "^18.0.0"
"undici": "7.27.2",
"yargs": "^18.0.0",
"zod": "3.25.76"
},
"devDependencies": {
"@types/cors": "^2.8.19",
Expand Down
33 changes: 33 additions & 0 deletions src/aem/aem.auth.schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { z } from 'zod';

// ----------------------------------------------------------------------------
// CertParamsSchema — shape validation for cert-auth CLI / env inputs (feat #4)
// ----------------------------------------------------------------------------
//
// Validates the cert-auth inputs BEFORE they reach `CertAuthStrategy.init()`.
// Shape-only — file reads, PEM-format checks, keypair validation, encrypted-
// key detection, and Agent construction all live in `CertAuthStrategy.init()`
// (feat #3). This schema catches the cheap-to-detect mistakes:
// - Empty-string flags (`--cert ""`)
// - Partial config (cert without key, or key without cert)
//
// `passphrase` intentionally has NO matching CLI flag — it MUST be sourced from
// the `AEM_KEY_PASSPHRASE` env var to keep the secret out of `ps aux` and
// shell history. The CLI surface only exposes `cert`, `key`, `ca`.
//
// Callers should use `safeParse(...)`, never `.parse()`, and must NOT echo
// `error.issues[].received` back to stdout/stderr — that would leak path
// values into CI logs.

export const CertParamsSchema = z
.object({
cert: z.string().min(1).optional(),
key: z.string().min(1).optional(),
ca: z.string().min(1).optional(),
passphrase: z.string().min(1).optional(),
})
.refine((d) => (d.cert && d.key) || (!d.cert && !d.key), {
message: '--cert and --key must be provided together',
});

export type CertParams = z.infer<typeof CertParamsSchema>;
Loading
Loading