基于 Bedrock AgentCore 的企业 MCP,集成 Okta SSO 与 Trino 功能
Lambda OAuth 脑部处理模块、PKCE 中继功能、AgentCore 网关与运行时系统,以及无需泄露共享令牌的按用户分配的 Trino 连接。
来源: Building Enterprise MCP on AWS Bedrock AgentCore: Okta SSO + Trino
本指南将逐步构建从原始材料到可运行系统的完整流程,用于在 AWS Bedrock AgentCore 上搭建企业级 MCP:Okta SSO + Trino。重点在于可操作的步骤、明确的检查点,以及可直接放入代码仓库的代码,无需猜测其用途。 在修改代码之前,应先明确输入参数、各步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新运行相应步骤,而无需猜测隐藏的状态。 除了功能结果外,还需记录执行时间以及令牌或查询的成本。提前了解成本情况,可避免在流程从演示环境过渡到共享环境时出现意外账单。
简而言之
简而言之,在修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能开关应集中存放,以便操作人员无需查看整个流程即可进行审核。 提供具有严格结构定义和明确副作用标签的工具。主机在自动批准之前需要知道哪些调用会修改状态。
MCP Client → Lambda OAuth Proxy → Bedrock AgentCore Gateway → AgentCore Runtime → Trino
为何将原始令牌交给工具会导致企业级检查失败
为了解释为何将原始令牌传递给工具会导致企业级检查失败,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的功能。 应使用结构严格的工具,并为这些工具添加明确的副作用标签。在自动批准之前,系统管理员需要知道哪些调用会改变状态。
架构概览
为便于快速了解架构设计,应在修改代码之前明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。相比冗长的脚本,更应采用小型且可测试的单元。当某个步骤失败时,故障原因应能指向单一责任模块,而非复杂的流程链。工具的设计应具备明确的架构规范和清晰的副作用标识,这样主机在自动批准操作之前就能知道哪些调用会改变系统状态。为便于快速了解架构设计,应在修改代码之前明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。除了功能结果外,还应记录执行时间以及令牌或查询的成本。提前了解成本情况,可避免在流程从演示环境转向共享环境时出现意外费用。
1. MCP客户端
对于MCP客户端,在修改代码之前需先明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。配置信息应置于应用程序代码之外,环境文件、密钥存储以及功能标志都应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审核。认证应在网关层面完成,重新授权则在数据层进行——仅凭承载令牌并不足以界定租户边界。
2. Lambda MCP代理(OAuth的核心组件)
对于第2点:Lambda MCP代理(即OAuth处理模块),在修改代码之前需明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。需同时记录正常流程与异常恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的组成部分,而非后续需要补充的功能。应在网关处进行身份验证,在数据层进行重新授权——仅凭承载令牌并不能作为租户边界。
3. AWS Bedrock AgentCore网关
对于第3点:AWS Bedrock AgentCore网关,在修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。相比庞大的脚本,更应采用小型且可测试的单元。当某个步骤失败时,故障原因应能明确指向某个特定职责,而非整个复杂的流程。应在网关处进行身份验证,在数据层面重新授权——仅凭承载令牌并不足以界定租户边界。对于第3点:AWS Bedrock AgentCore网关,在修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。除了功能结果外,还应记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在流程从演示环境转向共享环境时出现意外账单。
4. AWS Bedrock AgentCore 运行时
对于 4. AWS Bedrock AgentCore 运行时,应在修改代码之前明确输入参数、该步骤的负责人以及终止条件。操作员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,这样操作员无需查看整个系统结构即可进行审核。 需为每次调用记录工具名称、参数哈希值、延迟时间以及执行结果。若没有这些记录,调试代理循环将会耗费大量时间。
端到端认证流程
对于端到端的认证流程,在修改代码之前需明确输入参数、各步骤的负责人以及结束条件。操作人员应能够从已知的检查点重新执行该步骤,而无需猜测隐藏状态。 需同时记录正常流程与异常恢复流程。重试机制、人工审核环节以及死信处理都是产品功能的一部分,而非后续需要补充的内容。 需为每次调用记录工具名称、参数哈希值、延迟时间以及执行结果。没有这些记录,调试代理将陷入无止境的循环,耗费大量时间。
┌─────────────────────┐
│ MCP Client │
│ Claude / Claude Code│
└──────────┬──────────┘
│
│ OAuth 2.1 + PKCE
▼
┌─────────────────────┐
│ Lambda MCP Proxy │◄──────────────► Okta
│ OAuth / Identity │ Login + JWT
└──────────┬──────────┘
│
│ Bearer JWT
▼
┌─────────────────────────────┐
│ Bedrock AgentCore Gateway │
│ CUSTOM_JWT validation │
└────────────┬────────────────┘
│ SigV4
▼
┌─────────────────────────────┐
│ Bedrock AgentCore Runtime │
│ FastMCP Server │
│ │
│ list_catalogs │
│ describe_table │
│ execute_query │
└────────────┬────────────────┘
│
│ User JWT
▼
┌─────────────────────────────┐
│ Enterprise Query Engine │
│ Per-user authorization │
└─────────────────────────────┘
深入探讨1:让Lambda代理支持OAuth 2.1协议
在“深入探讨1:让Lambda代理支持OAuth 2.1”中,修改代码之前需先明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 建议使用小型、可测试的单元而非冗长的脚本。当某个步骤失败时,故障应指向单一责任点,而非复杂的流程链。 需为每次调用记录工具名称、参数哈希值、延迟时间以及执行结果。没有这些记录,调试循环会耗费大量时间。 在“深入探讨1:让Lambda代理支持OAuth 2.1”中,修改代码之前需先明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解成本情况可避免在流程执行过程中出现意外账单。
从演示环境过渡到共享环境。# GET /.well-known/protected-resource
{
"resource": f"{BASE_URL}/mcp",
"authorization_servers": [BASE_URL],
}
# GET /.well-known/oauth-authorization-server
{
"issuer": BASE_URL,
"authorization_endpoint": f"{BASE_URL}/authorize",
"token_endpoint": f"{BASE_URL}/token",
"registration_endpoint": f"{BASE_URL}/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
}
在不破坏功能的前提下中继 PKCE
为在不破坏功能的情况下中继 PKCE,应在修改代码之前明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,以便操作人员无需查看整个流程即可进行审计。 提供具有严格结构定义和明确副作用标签的工具。主机需要在自动批准之前知道哪些调用会修改状态。
relay_payload = json.dumps({"u": original_redirect, "s": original_state}).encode()
relay_state = base64.urlsafe_b64encode(relay_payload).decode().rstrip("=")
params = {
"client_id": OKTA_CLIENT_ID,
"response_type": "code",
"redirect_uri": f"{BASE_URL}/callback", # <- proxy's callback
"state": relay_state, # <- client's state, wrapped
"code_challenge": qs["code_challenge"], # <- untouched
"code_challenge_method": "S256",
}
动态客户端注册
对于动态客户端注册,应在修改代码之前明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及死信处理都是产品功能的一部分,而非后续需要补充的内容。 应提供具有严格结构定义和明确副作用标识的工具。在自动批准之前,主机需要知道哪些调用会改变系统状态。
def handle_register(event):
req = json.loads(event.get("body") or "{}")
return _json_resp(201, {
"client_id": OKTA_CLIENT_ID,
"redirect_uris": req.get("redirect_uris", []),
"grant_types": req.get("grant_types", ["authorization_code"]),
"response_types": req.get("response_types", ["code"]),
"token_endpoint_auth_method": "none",
})
深入探讨2:FastMCP置于AgentCore之后时出现的故障
在深度探讨2中:当FastMCP位于AgentCore之后时会出现故障,因此在修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 相较于庞大的脚本,应优先使用小型且可测试的单元。当某个步骤失败时,故障原因应能指向单一责任模块,而非复杂的流程链。 应提供具有明确结构规范和清晰副作用标签的工具。主机在自动批准之前需要知道哪些调用会修改状态。 在深度探讨2中:当FastMCP位于AgentCore之后时会出现故障,因此在修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 应在功能结果旁记录执行时间以及令牌或查询成本。尽早让成本信息可见有助于预防问题。
当路径从演示环境切换到共享环境时,会出现意外的费用。尖锐问题 #1:没有自定义 HTTP 请求头能传递到容器中
针对“尖锐问题 #1:没有自定义 HTTP 请求头能传递到容器中”这一情况,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志都应集中存放于一个位置,这样操作人员无需查看整个流程即可进行审核。 在网关处进行身份验证,在数据层再次授权。仅凭承载令牌并不足以作为租户边界。
meta = payload.setdefault("params", {}).setdefault("_meta", {})
meta["okta_token"] = token # the validated user identity
meta["trino_target"] = routing_hint # optional routing metadata
class CredentialExtractMiddleware:
async def __call__(self, scope, receive, send):
# buffer the whole body
body = await _read_full_body(receive)
# pull identity out of params._meta
meta = json.loads(body).get("params", {}).get("_meta", {})
token = meta.get("okta_token") # AgentCore also injects the validated caller token as a header —
# prefer that when present, fall back to _meta / Authorization otherwise
token = token or _caller_token_from_headers(scope) _req_okta_token.set(token) # replay the buffered body so the MCP app can read it too
await self.app(scope, _replay(body, receive), send)
尖锐问题 #2:AgentCore 会移除 Accept 请求头
对于“锐利边缘#2”:AgentCore会在修改代码之前移除Accept请求头,明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。
需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的组成部分,而非后续需要补充的功能。
在网关处进行身份验证,在数据层再次授权。仅凭承载令牌并不足以界定租户边界。
class InjectAcceptMiddleware:
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
headers = [(k, v) for k, v in scope["headers"] if k != b"accept"]
headers.append((b"accept", b"application/json, text/event-stream"))
scope = {**scope, "headers": headers}
await self.app(scope, receive, send)
深入探讨3:将Okta访问令牌映射到独立的Trino会话
在“深入探讨3:将Okta访问令牌映射到独立的Trino会话”中,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 建议使用小型、可测试的单元而非庞大的脚本。当某个步骤失败时,故障应能指向具体的责任方,而非整个复杂的流程。 在网关处进行身份验证,在数据层再次授权。仅凭承载令牌并不能作为租户边界。 在“深入探讨3:将Okta访问令牌映射到独立的Trino会话”中,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 除了功能结果外,还需记录执行时间以及令牌或查询的成本。提前了解成本情况可避免出现意外的费用账单。
当路径从演示环境切换到共享环境时。def get_connection():
token = _req_okta_token.get()
user = _jwt_email(token) # e.g. "alice@company.com"
return backend.connect(
host=BACKEND_HOST,
user=user, # identity for attribution/authorization
auth=JWTAuthentication(token), # the backend re-validates this itself
http_scheme="https",
)
授权与只读限制最终应体现在数据层。
为了确保授权与只读限制真正落实在数据层,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审计。 需为每次调用记录工具名称、参数哈希值、延迟时间以及执行结果。若没有这些记录,调试代理将陷入无限循环,耗费大量时间。
forbidden = ("INSERT", "UPDATE", "DELETE", "DROP", "CREATE",
"ALTER", "TRUNCATE", "MERGE", "REPLACE", "GRANT", "REVOKE")
if any(query.strip().upper().startswith(kw) for kw in forbidden):
raise ValueError("Write operations are not allowed.")
连接客户端的两种方式
对于连接客户端的两种方式,在修改代码之前需先定义输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的功能。 每次调用都需记录工具名称、参数哈希值、延迟时间以及执行结果。没有这些记录,调试代理将陷入无限循环,耗费大量时间。
{ "mcpServers": { "data": { "type": "http", "url": "https://<lambda-url>/mcp" } } }
{ "mcpServers": { "data": { "type": "http", "url": "http://localhost:8766/mcp" } } }
我们学到的经验
根据我们的经验,在修改代码之前应明确输入参数、该步骤的负责人以及终止标准。操作人员应当能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。相比冗长的脚本,更应选择小型且易于测试的单元。当某个步骤失败时,故障原因应能指向具体的责任方,而非复杂的流程链。每次调用都需记录工具名称、参数哈希值、延迟时间以及执行结果;如果没有这些记录,调试过程将会浪费大量时间。根据我们的经验,在修改代码之前应明确输入参数、该步骤的负责人以及终止标准。操作人员应当能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。在功能结果之外,还需记录执行时间以及令牌或查询成本。提前了解这些成本信息,可以避免在从演示环境过渡到共享环境时出现意外的费用支出。
总结
最后,在修改代码之前,需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。配置信息应置于应用程序代码之外,环境文件、密钥存储及功能标志应集中存放于一个操作人员可审核的位置,无需阅读整个流程图。此外,应提供具有明确结构规范和显式副作用标签的工具,以便主机在自动批准之前了解哪些调用会修改状态。
操作检查清单
在制定操作检查清单时,同样需要在修改代码之前明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。
将此阶段视为输入与已验证输出之间的契约。为相关成果命名,明确成功判定标准,杜绝默许的半完成状态。
在网关处进行身份验证,在数据层再次授权。仅凭承载令牌并不能构成租户边界。
在耗时较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次计费相同的大型语言模型调用。
将身份信息从客户端传递到下游存储系统,而无需将所有用户统一归为服务账户。
在预算允许的情况下,使用测试数据而非真实的付费 API,在持续集成过程中添加用于检测关键路径的冒烟测试。
在推广该技术栈之前,应先冻结版本、为关键路径生成标准记录,并确认回滚步骤。共享环境需要设置速率限制、进行租户检查,同时明确密钥轮换的负责人。与其展示花哨的一次性演示,不如注重扎实的可靠性。
部署注意事项1:在更大范围推广之前,先锁定镜像版本、设定请求预算,并在测试环境中验证租户隔离效果。
部署注意事项2:在更大范围推广之前,先锁定镜像版本、设定请求预算,并在测试环境中验证租户隔离效果。
部署注意事项3:在更大范围推广之前,先锁定镜像版本、设定请求预算,并在测试环境中验证租户隔离效果。
部署注意事项4:在更大范围推广之前,先锁定镜像版本、设定请求预算,并在测试环境中验证租户隔离效果。
部署注意事项5:在更大范围推广之前,先锁定镜像版本、设定请求预算,并在测试环境中验证租户隔离效果。
部署注意事项6:在更广泛推广之前,先在测试环境中锁定图像、设置请求预算,并验证租户隔离功能。