feat: cross-account AWS API via managed MCP Server - #20
Open
CodeOfCloud wants to merge 1 commit into
Open
Conversation
CodeOfCloud
force-pushed
the
feat/cross-account-aws-mcp
branch
9 times, most recently
from
August 3, 2026 11:17
bc8aa13 to
664a70f
Compare
CodeOfCloud
force-pushed
the
feat/cross-account-aws-mcp
branch
from
August 3, 2026 11:24
664a70f to
a030ffa
Compare
Add a managed mode to lambda-proxy, enabled by aws_mcp_endpoint, that replaces the AgentCore Runtime + aws-api-mcp-server container path. The upstream container entered end-of-development on 2026-07-15 (removal 2027-07-15) and its Marketplace listing is closed to new subscriptions, so a fresh deployment cannot stand that target up at all. The managed AWS MCP Server offers the same any-API access with no container, subscription, or per-account runtime. The proxy signs MCP requests with SigV4 and exposes run_script instead of the deprecated call_aws. Passing account_id assumes a role in that member account, so the API executes in the member context and CloudTrail logs land there. The legacy Runtime path is preserved byte-for-byte when aws_mcp_endpoint is unset, keeping rollback to a configuration change. Beyond the API tools, the managed server also serves knowledge tools, so the proxy forwards three more: get_aws_skill, search_documentation and read_documentation. These read AWS documentation rather than account resources, so they run on the proxy's own credentials and account_id is stripped — no member-account AssumeRole. get_aws_skill retrieves AWS-authored workflows such as aws-billing-and-cost-management, which encodes procedures and the mistakes models make on cost data. Read access now comes from a per-account finops-readonly role that the proxy assumes for every query, including against its own account, so the proxy execution role becomes a pure pipe. examples/member-finops-readonly-role.yaml creates that role in every account, the Gateway's own included — one definition rather than a Terraform copy and a CloudFormation copy that can drift. PermissionsMode selects the grant: ReadOnlyAccess by default, or a scoped policy that allows inventory and cost reads while denying the actions that return stored data. Two non-obvious choices in the role's trust policy: - Access is restricted by aws:PrincipalArn rather than sts:ExternalId. An External ID addresses the confused-deputy problem when a third party cannot be identified by ARN; within one organization, naming the single permitted caller is stronger and removes a shared secret from the deployment path. ExternalId remains available and layers on top. - The proxy role is matched in a condition rather than named in Principal, where a role ARN resolves to a hidden unique ID: it would have to exist before member accounts are provisioned, and would break if the role were recreated. A condition ARN is a plain string comparison. Tool names use get_ / list_ / read_ / search_ prefixes for reads. MCP marks read-only tools with annotations.readOnlyHint and the managed server sets it, but Gateway's ToolDefinition accepts only name, description, inputSchema and outputSchema, so the hint cannot be forwarded. Clients then infer intent from the name — one classified retrieve_skill as a write and prompted on every call despite it only reading documentation. run_script keeps its name, since it executes model-authored code and is annotated destructiveHint upstream. docs/migrate-to-managed-mode.md covers upgrading an existing deployment. It was written against a real migration, walked end to end, and revised for what that surfaced: the tool schema file has to move together with the variable or the Gateway advertises tools the proxy no longer serves; the role is needed in the Gateway account too, not only in members; PermissionsMode has to be chosen before deploying rather than after; verification has to drive the agent before reading logs, since the filter cannot match anything earlier. Covered by 12 offline unit tests. Verified end to end against a live AWS Organization: all five tools through the Gateway, cross-account and local queries returning distinct resources, both PermissionsMode values deployed and their denials confirmed, and a bare Lambda invoke still refusing to enumerate accounts. Plans are non-destructive in managed mode and additive-free in legacy mode; cfn validate-template passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CodeOfCloud
force-pushed
the
feat/cross-account-aws-mcp
branch
from
August 4, 2026 11:49
091bac0 to
59758eb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a managed mode to
lambda-proxy(enabled byaws_mcp_endpoint) that replaces the AgentCore Runtime +aws-api-mcp-servercontainer path. The upstream container entered end-of-development on 2026-07-15 (removal 2027-07-15, awslabs/mcp#4115) and its Marketplace listing is closed to new subscriptions, so new deployments currently can't stand this target up. Managed mode provides the same any-API access with no container, subscription, or per-account runtime.run_scriptinstead of the deprecatedcall_awsaws_mcp_endpointis unset — existing deployments are unaffected, and rollback is a single variablerun_scriptaccepts an optionalaccount_id: the proxy assumesrole/<member_role_name>in that account and signs with the member credentials, so APIs execute in the member context and CloudTrail logs land in the member accountlist_member_accountstool resolves member account names to IDs viaorganizations:ListAccountsget_aws_skill,search_documentation,read_documentation. These read AWS documentation rather than account resources, so they run on the proxy's own credentials andaccount_idis stripped — no member-account AssumeRole.get_aws_skillretrieves AWS-authored workflows such asaws-billing-and-cost-management, which encodes procedures and the mistakes models make on cost dataPer-account
finops-readonlyroleThe proxy assumes this role for every query — including queries against the Gateway's own account (
run_scriptwith noaccount_id) — so its execution role becomes a pure pipe (STS + Organizations only) and payer and member queries share one permission model.examples/member-finops-readonly-role.yaml— the proxy assumes the role here tooPermissionsModeselects the grant, and applies to every account since one template covers them all. It defaults toReadOnlyAccess, whose read actions also return stored data (S3 objects, DynamoDB items, decrypted SSM parameters).InventoryOnlyinstead creates a scoped policy allowing inventory and cost reads while denying those that return content.An earlier revision built the Gateway account's role in Terraform and member roles from the template. That drifted: only the template could produce the scoped policy, so the Gateway account was stuck on
ReadOnlyAccesswith no supported way to tighten it.terraform/finops-readonly-role.tfis gone and the template covers every account, which also avoids maintaining the same 98-action policy in both HCL and YAML.Only the proxy's execution role can assume it, enforced with an
aws:PrincipalArncondition. The condition form is deliberate: a role ARN placed inPrincipalresolves to a hidden unique ID, so it must exist before member accounts are provisioned and breaks if the role is recreated.Naming read-only tools
MCP lets a server mark a tool read-only via
annotations.readOnlyHint, and the managed AWS MCP Server sets it. Gateway cannot forward it:ToolDefinitionaccepts onlyname,description,inputSchemaandoutputSchema, so passingannotationsfails parameter validation outright.Clients then have to infer intent, and at least one infers it from the tool name. Amazon Quick classified the upstream name
retrieve_skillas a write and prompted for approval on every call, despite it only reading documentation; exposing it asget_aws_skillmakes Quick classify it as a read, which allows blanket approval. The upstream tool name is unchanged — the proxy already maps its own tool names to upstream ones.docs/mcp-tools-reference.mdrecords the convention and two limits on it: this is observed client behaviour rather than a documented contract, and a tool with genuine side effects should not be renamed to dodge a prompt.run_scriptkeeps its name, since it executes model-authored code and is annotateddestructiveHintupstream.Docs
docs/migrate-to-managed-mode.mdwalks an existing deployment through the upgrade. Two steps are called out because both fail silently: a stale MCP client tool list makes the agent look healthy while all AWS API access is dead, and leavinglambda_timeoutat 30s truncates multi-region sweeps into partial results.Test plan
tests/unit/test_proxy_managed_mode.py) — SigV4 signing, tool routing, credential resolution, error propagation, and that knowledge-tool routing stripsaccount_idlist_member_accounts, same-account and cross-accountrun_script, all-region inventory sweepUnknown or unresolvable toolrather than enumerating accountscost-explorer-mcpandathena-mcpunaffected; mixed Cost Explorer +run_scriptorchestration works in one turnterraform planagainst a deployed stack destroys nothing; withaws_mcp_endpointunset it adds nothingPermissionsModevalues deployed to a member account and exercised through the Gateway: inventory and cost reads succeed underInventoryOnlywhile object reads, table scans, instance user data and decrypted parameters are denied; switching back toReadOnlyrestores the managed policy and removes the scoped onerun_scriptreturn distinct resources, and a bare Lambda invoke still refuses to enumerate accountscall_awsstill served traffic through the Runtime, then followed the guide step by step as written. Nine defects surfaced and are fixed here — the largest being that step 1 never said the tool schema file has to move with the variable, which leaves the Gateway advertising tools the proxy no longer servesaws cloudformation validate-templatepasses on the member template;terraform fmt/validateclean;ruff checkclean on changed filesNotes for reviewers
aws_mcp_endpoint == ""would drop the Marketplace dependency entirely — happy to do that here or as a follow-up, whichever you prefer.examples/is a new top-level directory; if you'd rather it live underterraform/ordocs/, say the word.