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
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ An MCP (Model Context Protocol)-enabled agent for Cloud Financial Management (CF

![Architecture Diagram](docs/images/finops-agent-architecture.png)

All Gateway targets are **Lambda functions**. The `lambda-proxy` Lambda forwards requests to the Bedrock AgentCore Runtime which hosts the aws-api-mcp-server container.
All Gateway targets are **Lambda functions**. The `lambda-proxy` Lambda has two modes: **managed mode** (recommended; set `aws_mcp_endpoint`) signs requests with SigV4 and forwards them to the [managed AWS MCP Server](https://docs.aws.amazon.com/agent-toolkit/latest/userguide/getting-started-aws-mcp-server.html), optionally assuming a role in a member account first for cross-account queries; **legacy mode** (default when `aws_mcp_endpoint` is unset) forwards to a Bedrock AgentCore Runtime hosting the aws-api-mcp-server container.

## Deployment Modes

Expand Down Expand Up @@ -48,9 +48,45 @@ Two distinct cross-account flows, each on its own row:

A single `make deploy` creates resources in both accounts. Terraform auto-generates an [External ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html) to secure the assumed role (stored in Terraform state).

### Cross-account resource queries (managed mode)

With managed mode enabled, the agent can also run **read-only AWS API queries in any member account** — e.g. "list EC2 instances and tags in account X". The `lambda-proxy` exposes two tools:

- `list_member_accounts` — resolves member account names to IDs via `organizations:ListAccounts` (deploy in the org management account to use this)
- `run_script(code, account_id?)` — executes sandboxed Python against AWS APIs through the managed AWS MCP Server; with `account_id`, the proxy first assumes `arn:aws:iam::<account_id>:role/<member_role_name>` and signs the request with the member credentials, so the API runs in the member context and CloudTrail logs land in the member account

Every account the agent queries — **including the Gateway's own account** — needs one IAM role (default name `finops-readonly`) with the `ReadOnlyAccess` managed policy, trusting this account and restricted to the proxy's execution role via an `aws:PrincipalArn` condition. There is no shared secret to distribute; see [how access is restricted](docs/migrate-to-managed-mode.md#how-access-is-restricted). The proxy's own execution role is a pure pipe (STS + Organizations only); all AWS read access flows through this per-account role, so local and member queries share one permission model.

Enable managed mode in `terraform.tfvars`:

```hcl
aws_mcp_endpoint = "https://aws-mcp.us-east-1.api.aws/mcp"
member_role_name = "finops-readonly" # optional, this is the default
lambda_timeout = 120 # multi-region inventory exceeds the 30s default
```

Then create the roles:

| Account | How |
| ------- | --- |
| This account (Gateway) | Deploy [`examples/member-finops-readonly-role.yaml`](examples/member-finops-readonly-role.yaml) to it — the proxy assumes the role here too |
| Each member account | The same template, once per account, or org-wide via a CloudFormation StackSet |

The member template takes this account's ID and the proxy's role name — the same values for every account, so one StackSet covers the organization:

```bash
terraform -chdir=terraform output -raw proxy_role_name
```

Without the role in a given account, queries against it fail with a message naming the missing role — the agent reports the gap rather than returning partial data.

The role's permissions are selectable. The default attaches `ReadOnlyAccess`, which covers any service the agent might be asked about; the template's `PermissionsMode=InventoryOnly` instead grants resource and cost metadata while denying the read actions that return stored data. See [choosing the permissions grant](docs/migrate-to-managed-mode.md#choosing-the-permissions-grant).

Already running a Runtime-based deployment? See [Migrate to Managed Mode](docs/migrate-to-managed-mode.md).

## Prerequisites

1. **AWS Marketplace Subscription** - [Subscribe to aws-api-mcp-server](https://aws.amazon.com/marketplace/pp/prodview-lqqkwbcraxsgw) (free, accept terms). For cross-account deployments, subscribe from the **data collection account**.
1. **AWS Marketplace Subscription** — *legacy mode only*: [aws-api-mcp-server](https://aws.amazon.com/marketplace/pp/prodview-lqqkwbcraxsgw) requires an existing subscription (the listing is closed to new subscriptions, and the upstream server is [scheduled for removal in July 2027](https://github.com/awslabs/mcp/issues/4115)). New deployments should use **managed mode** instead (`aws_mcp_endpoint = "https://aws-mcp.us-east-1.api.aws/mcp"`), which needs no subscription or container.
2. **CUR 2.0 Export** - [Create a Cost and Usage Report 2.0](https://docs.aws.amazon.com/cur/latest/userguide/cur-create.html) export to Amazon S3 with Athena integration enabled. Ensure the S3 bucket has Block Public Access enabled and server-side encryption configured.
3. **Identity Provider (IdP)** — *optional*: only needed if you switch to `gateway_auth_type = "CUSTOM_JWT"`. The default (`COGNITO`) auto-provisions a Cognito User Pool + OAuth client for service-to-service callers (QuickSuite, n8n, CI) — no external IdP required. See [Identity Provider Setup](#identity-provider-setup).
4. **AWS CLI Profiles** - [Named profiles](https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html) configured for target account(s)
Expand All @@ -62,7 +98,7 @@ This deploys the AWS FinOps Agent infrastructure:
- AgentCore Gateway with JWT authentication
- AWS Lambda functions (cost-explorer-mcp, athena-mcp, lambda-proxy)
- IAM roles and policies (including the management-account role consumed by `cost-explorer-mcp`, if cross-account mode is configured)
- AgentCore Runtime (aws-api-mcp-server container)
- AgentCore Runtime (aws-api-mcp-server container) — legacy mode only; managed mode replaces it with the managed AWS MCP Server

**Not included:** QuickSuite requires manual setup after deployment. See [QuickSuite Agent Setup](docs/quicksuite-agent-setup.md).

Expand Down Expand Up @@ -223,7 +259,7 @@ After deployment, configure your MCP client (QuickSuite) to connect to the gatew

| Target | Description |
| ------------------------------ | ------------------------------------------------- |
| `aws-api-mcp` | AWS API MCP server (Marketplace) — `call_aws`, `suggest_aws_commands` |
| `aws-api-mcp` | Cross-account AWS API access plus AWS documentation and expert skills, via managed AWS MCP Server — `run_script`, `list_member_accounts`, `get_aws_skill`, `search_documentation`, `read_documentation` (legacy: `call_aws` via AgentCore Runtime when managed mode is disabled) |
| `cost-explorer-mcp` | AWS Cost Explorer API (6 tools) |
| `athena-mcp` | Athena queries (8 tools) |

Expand All @@ -237,6 +273,7 @@ After deployment, configure your MCP client (QuickSuite) to connect to the gatew
| [Configuration](docs/configuration.md) | tfvars, permissions, make commands |
| [Troubleshooting](docs/troubleshooting.md) | Debugging, logs, common issues |
| [QuickSuite Agent Setup](docs/quicksuite-agent-setup.md) | Configure CFM agent in QuickSuite |
| [Migrate to Managed Mode](docs/migrate-to-managed-mode.md) | Upgrade an existing Runtime-based deployment |

## Testing

Expand Down
41 changes: 24 additions & 17 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,43 @@ The FinOps MCP Gateway deploys an Amazon Bedrock AgentCore Gateway that exposes

```
┌──────────────┐ ┌─────────────────────────────────────────────────────────────────────────┐
│ MCP Client │ Federate JWT │ AWS Cloud
│ MCP Client │ Federate JWT │ AWS Cloud (payer account)
│ │────────────────>│ │
│ QuickSuite │ │ ┌─────────────┐ ┌──────────────────────────────────────────────┐ │
│ │<────────────────│ │ AgentCore │ │ Lambda Targets │ │
│ │ │ │ Gateway │ │ │ │
└──────────────┘ │ │ │──────>│ cost-explorer-mcp ───> Cost Explorer API │ │
│ │ │ │ athena-mcp ──────────> Athena + S3 + AWS Glue │ │
│ │ │ │ athena-mcp ──────────> Athena + S3 + Glue │ │
│ │ │ │ │ │
│ │ │ │ lambda-proxy ────────────────┐ │ │
│ └─────────────┘ └───────────────────────────────┼──────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────┐ │
│ │ AgentCore Runtime │ │
│ │ (aws-api-mcp-server from AWS Marketplace) │ │
│ │ │ │
│ │ Tools: call_aws, suggest_aws_commands │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│ │ │ │ lambda-proxy (managed mode) │ │
│ └─────────────┘ │ ├ list_member_accounts ─> Organizations │ │
│ │ └ run_script(code, account_id?) │ │
│ │ │ no account_id: own credentials │ │
│ │ │ account_id: AssumeRole into member ───┼─┼──┐
│ └──────┼───────────────────────────────────────┘ │ │
└───────────────────────────────┼─────────────────────────────────────────┘ │
│ SigV4 (caller's credentials) │
v │
┌───────────────────────────────────────────────┐ ┌────────────────────────┴──┐
│ AWS MCP Server (managed by AWS) │ │ Member account │
│ aws-mcp.<region>.api.aws/mcp │ │ IAM role finops-readonly │
│ Verifies SigV4, forwards the request with │ │ (ReadOnlyAccess, trusts │
│ the caller's credentials — APIs execute in │ │ payer + ExternalId) │
│ that credential's account context │ └───────────────────────────┘
└───────────────────────────────────────────────┘
```

All Gateway targets are **Lambda functions**. The `lambda-proxy` Lambda forwards requests to the AgentCore Runtime which hosts the aws-api-mcp-server container from AWS Marketplace.
All Gateway targets are **Lambda functions**. In **managed mode** (recommended; `aws_mcp_endpoint` set), the `lambda-proxy` signs MCP requests with SigV4 and forwards them to the managed AWS MCP Server; passing `account_id` makes it assume the member-account role first, so the API executes in the member context. In **legacy mode** (`aws_mcp_endpoint` unset), it forwards to an AgentCore Runtime hosting the aws-api-mcp-server container (deprecated upstream, removal July 2027).

## Components

| Component | Description |
|-----------|-------------|
| **AgentCore Gateway** | MCP endpoint with Federate JWT authentication. Routes requests to Lambda targets. |
| **AgentCore Runtime** | Hosts the aws-api-mcp-server container from AWS Marketplace. Provides `call_aws` and `suggest_aws_commands` tools. |
| **lambda-proxy** | Lambda that forwards MCP requests to AgentCore Runtime. |
| **lambda-proxy** | Lambda with two modes. Managed mode: exposes `run_script` (sandboxed Python via the managed AWS MCP Server, optional `account_id` for cross-account) and `list_member_accounts`. Legacy mode: forwards MCP requests to an AgentCore Runtime. |
| **AWS MCP Server (managed)** | AWS-hosted MCP endpoint. Authenticates SigV4, forwards each request with the caller's credentials; downstream services authorize against that credential's own IAM policies. |
| **Per-account role** | One `finops-readonly` IAM role per account — members **and the payer itself** (ReadOnlyAccess; trusts the payer with an External ID). The proxy assumes it for every query, so all read access shares one permission model; the only member-side footprint. |
| **AgentCore Runtime** | Legacy mode only. Hosts the aws-api-mcp-server container (`call_aws`, `suggest_aws_commands`). |
| **cost-explorer-mcp** | Lambda implementing MCP protocol for Cost Explorer API (6 tools). |
| **athena-mcp** | Lambda implementing MCP protocol for Athena queries (8 tools). |
| **test-mcp** | Dummy Lambda for Gateway verification (`hello`, `echo`). |
Expand All @@ -53,7 +60,7 @@ All Gateway targets are **Lambda functions**. The `lambda-proxy` Lambda forwards

| Target Name | Purpose |
|-------------|---------|
| `aws-api-mcp` | Forwards to AgentCore Runtime for AWS CLI execution |
| `aws-api-mcp` | Managed mode: `run_script` (cross-account via `account_id`) + `list_member_accounts`. Legacy mode: forwards to AgentCore Runtime for AWS CLI execution |
| `cost-explorer-mcp` | AWS Cost Explorer API access |
| `athena-mcp` | Athena query execution |
| `test-mcp` | Gateway verification (dummy) |
Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ aws_region = "us-east-1"
# MCP Server version from AWS Marketplace
mcp_server_image_version = "1.2.0"

# Lambda settings
# Lambda settings (with managed AWS MCP mode, use lambda_timeout >= 120 —
# multi-region run_script inventory scripts routinely exceed 30s)
lambda_timeout = 30
lambda_memory_size = 256

Expand Down
41 changes: 36 additions & 5 deletions docs/mcp-tools-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,43 @@ All available MCP tools organized by Gateway target.

**Target Name**: `aws-api-mcp`

The `lambda-proxy` Lambda forwards MCP requests to the **AgentCore Runtime** which hosts the `aws-api-mcp-server` container from AWS Marketplace.
The `lambda-proxy` Lambda forwards MCP requests to the **managed AWS MCP Server** for cross-account AWS API execution.

| Tool | Description |
|------|-------------|
| `call_aws` | Execute AWS CLI commands |
| `suggest_aws_commands` | Get AWS CLI command suggestions |
| `run_script` | Execute Python code against AWS APIs in a sandboxed environment using await call_boto3(...); supports cross-account execution via optional account_id |
| `list_member_accounts` | Resolve member account names to 12-digit account IDs; returns all accounts in the organization |
| `get_aws_skill` | Retrieve an AWS-authored expert workflow with procedures, correct API usage, and known pitfalls. `aws-billing-and-cost-management` covers cost analysis, commitment evaluation, right-sizing, budgets, and CUR queries |
| `search_documentation` | Search official AWS documentation and discover available skill names |
| `read_documentation` | Fetch a full AWS documentation page as markdown |

`run_script` and `list_member_accounts` act on account resources. The three
knowledge tools read AWS documentation instead, so they ignore `account_id` and
run under the proxy's own credentials — no member-account role is involved.

## Naming read-only tools

Prefer a `get_` / `list_` / `read_` / `search_` / `describe_` prefix for any tool
that only reads.

MCP lets a server mark a tool read-only with the `annotations.readOnlyHint`
field, and the managed AWS MCP Server does set it. Gateway cannot forward it:
[`ToolDefinition`](https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_ToolDefinition.html)
accepts only `name`, `description`, `inputSchema`, and `outputSchema`, so
supplying `annotations` fails parameter validation outright.

Without that hint a client has to infer intent, and at least one infers it from
the tool name. In Amazon Quick, tools classified as reads can be granted blanket
approval, while everything else prompts on every call. `get_aws_skill` is named
that way for this reason — it wraps the upstream `aws___retrieve_skill`, which is
annotated `readOnlyHint: true`, but under its original name Quick classified it
as a write and prompted on every call.

This is observed client behaviour, not a documented contract, so treat the
convention as a soft signal that may stop mattering once Gateway forwards
annotations. Do not rename a tool that genuinely has side effects in order to
dodge an approval prompt: `run_script` executes model-authored code and is
annotated `destructiveHint: true` upstream, so prompting on each call is correct.

## Cost Explorer MCP

Expand Down Expand Up @@ -62,9 +93,9 @@ Dummy Lambda for Gateway verification. Not intended for production use.

| Target | Tools |
|--------|-------|
| lambda-proxy (aws-api-mcp-server) | 2 |
| lambda-proxy (managed AWS MCP Server) | 5 |
| cost-explorer-mcp | 6 |
| athena-mcp | 8 |
| **Total** | **16** |
| **Total** | **19** |

(Excludes test-mcp dummy tools)
Loading