Lark AgentCore Gateway Interceptor MCP Server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Lark AgentCore Gateway Interceptor MCP Serverlist my documents"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Lark Identity on AgentCore — Gateway Interceptor
A reference implementation of enterprise identity on Amazon Bedrock AgentCore, using Lark (Feishu) as the identity provider. A simple agent is reachable from two Lark entrypoints — chat messages and a desktop-client-embedded web UI — that both resolve to the same lark:{open_id} identity. That identity is forwarded to downstream MCP tools through an AgentCore Gateway Request Interceptor (the agent never holds a downstream credential), and the tools then act as the user against Lark with the user's own token, so they reach only what that user can — Lark itself decides. In short, the agent inherits both who you are and what you're allowed to do, adding nothing of its own.
This is the Gateway Interceptor variant: downstream tools are Lambda targets, and a custom Gateway Request Interceptor forwards identity and injects the per-user credential (self-managed token store). The sibling repo lark-identity-on-agentcore-native achieves the same guarantees with the AgentCore Identity Token Vault (OAuth 3LO, driven agent-side) instead; the two differ only in how the downstream hop resolves per-user credentials.
Architecture
┌─────────────────────┐ Lark desktop web UI ──▶ SPA (S3/CloudFront)
Lark message ──▶ │ Router Lambda │ h5sdk requestAccess ──▶ login code
(webhook) │ verify/decrypt │ │
│ resolve user │ ▼
└──────────┬──────────┘ web_api Lambda
│ POST /api/lark/auth code ─▶ Cognito JWT (Lark is IdP)
│ POST /api/session JWT ─▶ presigned WSS URL
│ │
InvokeAgentRuntime (SigV4) │ │ browser opens WSS
payload carries actorId ▼ ▼ (platform bridges to /ws)
┌─────────────────────────────────────────────────────┐
│ Agent container (ARM64, AgentCore Runtime) │
│ :8080 /ping /invocations(POST) /ws(WebSocket) │
│ Strands Agent + AgentCore Memory (per-user STM) │
└───────────────────────┬─────────────────────────────┘
│ MCP call, Bearer = user's Cognito ACCESS token
▼
┌───────────────────┐
│ AgentCore Gateway │ MCP server; customJWTAuthorizer (Cognito)
│ + Interceptor λ │ passRequestHeaders=true; injects end-user id
└─────────┬─────────┘ (Gateway invokes its Lambda target, not Lark)
▼
┌───────────────────┐ whoami — identity proof
│ Tool Lambda │ list_my_docs / create / edit / delete — act AS the
└─────────┬─────────┘ user with THIS user's Lark user_access_token (by open_id)
│ HTTPS, Bearer = user_access_token
▼
┌─────────────────────────────────────────────────────┐
│ Lark REST API → returns only what THIS user can see │
└─────────────────────────────────────────────────────┘
Identity: both entrypoints resolve to lark:{open_id} (shared session/memory).
Lark is NOT standard OIDC → web_api exchanges the login code for a Cognito JWT.
Pass-through (whoami) proves WHO; inheritance (list_my_docs, user_access_token)
means the agent can only reach what the user can — Lark adjudicates.See docs/architecture.md for the full layered design, per-hop auth matrix, and sequence diagrams.
Related MCP server: MCP Server My Lark Doc
Layout
Path | What |
| CDK app (uv-managed deps) — 6 stacks |
| security, agentcore, router, webui, gateway, observability |
| Strands agent container (HTTP contract + WS + AgentCore Memory + MCP Gateway client) |
| Lark webhook: verify/decrypt/tenant-token/send |
| Lark login exchange + session bootstrap (presigned WSS) |
| Gateway Request Interceptor (per-user credential injection) |
| MCP tool targets: |
| Lark-embedded SPA (no build step; renders agent replies as Markdown via marked + DOMPurify) |
| deploy / setup-lark / manage-allowlist / test |
| Full architecture: layers, per-hop auth, sequence diagrams |
Deploy
Prereqs: uv, Docker, the AgentCore CLI (npm i -g @aws/agentcore), and AWS credentials.
Scripts default to the default profile / us-west-2; override with PROFILE=... REGION=....
The AgentCore Runtime and Gateway have no CloudFormation resources in this region:
the Gateway is created by deploy.sh via the control-plane CLI (IDs fed back into cdk.json),
the Runtime image is built (ARM64, CodeBuild) and deployed with the AgentCore CLI.
cp .env.example .env # fill in Lark appId/appSecret/encryptKey/token + your open_id
scripts/deploy.sh --base # CDK base stacks (security, agentcore, router, gateway, observability)
scripts/deploy.sh --gateway # create the MCP Gateway + interceptor + demo target
scripts/deploy.sh --runtime # build ARM64 image (CodeBuild) + deploy the Runtime (AgentCore CLI)
scripts/setup-lark.sh # read .env → Secrets Manager; print webhook/SPA URLs; allowlist you
scripts/deploy.sh --frontend # deploy WebUI stack, inject SPA config, upload SPA
# or: scripts/deploy.sh # run every step in orderConfig in cdk.json: default_model_id (global.anthropic.claude-sonnet-5),
lark_api_domain (https://open.larksuite.com international / open.feishu.cn),
registration_open (false = allowlist only), presigned_url_expires.
Tear down
scripts/destroy.sh # delete everything deploy.sh created (asks for confirmation)
# or: scripts/destroy.sh --yes # skip the promptDeletes in dependency order — Gateway targets → Gateway → Runtime (all CLI-created, so cdk destroy alone can't remove them) → the six CDK stacks → the dynamic per-user token secrets ({prefix}/user-tokens/*, created at runtime and not owned by any stack). Idempotent: re-running skips already-gone resources. Your Lark console app config is not touched; re-seed credentials from .env via scripts/setup-lark.sh on the next deploy.
Lark console setup (do this once, in order)
The webhook/SPA URLs come from deploy.sh output. In the Lark developer console:
Add features: enable Bot + Web app.
Permissions & Scopes — add all of these, then note that subscribing to the message event requires the p2p scope or single-chat messages are never pushed:
im:message,im:message:readonlyim:message.p2p_msg:readonly← required for single-chat messagesim:message:send_as_bot(reply),im:resource(images)contact:user.base:readonly(login → open_id)
For permission inheritance (the doc tools act as the user), also add these under the User Token Scopes tab — they are user-identity scopes and usually need admin approval before they take effect; the web app's
tt.requestAccessscopeListmust match them exactly or it fails with 20027:drive:drive(list/create/manage the user's Drive files),docx:document(create/edit docx),offline_access(refresh_token — the user_access_token lives only ~2h)
Events & Callbacks: subscription mode = Send to developer's server; set Request URL to the webhook URL; enable Encryption (note the Encrypt Key); add event
im.message.receive_v1.Security Settings: add the SPA URL to Redirect URLs and H5 trusted domains. Leave IP allowlist empty (Lambda egress IPs are dynamic).
Web app: set Desktop + Mobile homepage to the SPA URL.
Version Management & Release: create a version and publish. Any change to scopes/events requires a re-publish to take effect.
Then scripts/setup-lark.sh stores credentials and allowlists your open_id.
New users: they message the bot, get a rejection with their lark:ou_... id, and an admin runs scripts/manage-allowlist.sh add lark:ou_... (or set
registration_open: true to let anyone in).
Test
scripts/test.sh # agent (8) + router (7) + web_api (4)Cost
This deploys billable AWS resources. All the always-on pieces are consumption- or per-unit-priced (no fixed reservation), so an idle single-user demo in us-west-2 is on the order of a couple USD/month before model usage; the variable cost is dominated by the agent's Bedrock calls. Verify current rates on the AWS pricing pages — figures below are as researched, not a quote.
Bedrock model invocations — the main usage-sensitive line; priced per input/output token on the model in
default_model_id. A chatty demo is cents-to-dollars; a load test is not.AgentCore Runtime — metered per-second, not a reserved instance: CPU (
~$0.0895/vCPU-hour) is billed only during active processing (free while waiting on the model/tools), memory (~$0.00945/GB-hour) accrues continuously while the microVM is alive. An idle deployment still costs memory-time until the session's microVM is torn down.AgentCore Gateway — per-invocation (
~$0.005 per 1,000tool/list calls); negligible at demo volume.AgentCore Memory (STM) — billed per event written (
~$0.25 per 1,000create-event calls), not for retention duration; the 30-day window itself adds no storage fee.Lambda + API Gateway (HTTP + WSS) — router, web_api, interceptor, tools; effectively free at demo volume.
Secrets Manager —
$0.40/secret/montheach: the Lark-creds and tool-key secrets, plus one per-user token secret created on first authorization (so ~$1.20/month at one user, growing with users).Cognito, DynamoDB (on-demand) — the identity plane; negligible at demo volume.
S3 + CloudFront — SPA hosting; pennies at demo volume.
scripts/destroy.sh removes everything, including the dynamic per-user token secrets, so you aren't left paying for orphaned resources. Costs are usage-driven — an idle deployment is cheap, but leaving it up still accrues the per-secret charges and the Runtime's memory-time until its microVM shuts down.
Security considerations
This is a reference implementation, not production-ready as-is. Before any real use:
IAM is scoped but a sample. Secret access is path-scoped to
{prefix}/*; two grants useresources=["*"]only because the AWS actions (ecr:GetAuthorizationToken,secretsmanager:CreateSecret) cannot be resource-scoped — writes are still constrained to{prefix}/user-tokens/*. Re-review least-privilege for your account.CORS is wide open (
allow_origins=["*"]on the HTTP API and CloudFront). Lock it to your SPA origin for production.Per-user Lark tokens live in Secrets Manager, one secret per user (
{prefix}/user-tokens/{open_id}), refreshed on expiry. The agent never holds them — the tool Lambda loads them at call time. Treat the account hosting these as sensitive.The agent's output is untrusted — the web UI sanitizes it with DOMPurify before rendering, and the marked/DOMPurify CDN scripts are pinned with SRI. Keep it that way if you touch the render path.
Webhook verification is fail-closed — missing/invalid signature or a timestamp outside the replay window is rejected. Don't relax this.
No secrets in this repo — Lark credentials come from
.env→ Secrets Manager viascripts/setup-lark.sh;.envis git-ignored.
Deployment status
All four goals verified end-to-end on an AWS account:
✅ Lark chat: a real single-chat message → Router (verify + AES decrypt) → resolve
lark:{open_id}→ AgentCore Runtime → model reply back in Lark./whoamiin Lark reports the caller's identity.✅ Desktop embed: opening the Web app inside the Lark client runs h5sdk 免登 →
POST /api/lark/auth(code → Cognito JWT) →POST /api/session(presigned WSS) → the AgentCore platform bridges the browser's WSS to the agent's/wson port 8080 → streaming reply. Shows the user's display name. The minted JWT is cached insessionStorage, so a refresh reuses it and skipsrequestAccess— otherwise Lark re-prompts the consent popup on every load (it has no silent login-state reuse; the SPA must do it).✅ Unified identity: both entrypoints resolve to the same
lark:{open_id}(session/workspace shared).✅ MCP identity pass-through: the agent mints the user's Cognito access token → AgentCore Gateway (
customJWTAuthorizer) → Request Interceptor reads the identity and injects the per-tenant downstream key → thewhoamitool reports the real end-user id and that a credential was injected, while the agent never holds the key.✅ Conversation memory: the agent is a Strands agent with an AgentCore Memory (STM) session manager keyed by
(actor_id, session). Verified across two different runtime sessions for the same user: it recalls a fact stated earlier — so memory persists across reconnects and both entrypoints (30-day event retention).✅ Permission inheritance: after the user authorizes in the web app, the doc tools act as that user — each loads the user's Lark
user_access_token(stored by open_id, refreshed on expiry) and calls the Lark API, so access is scoped to what that user can see/do in Lark, adjudicated by Lark, and the agent never holds the token. Verified end-to-end:list_my_docsreturned the user's real folder and, given afolder_token, descended into it to list the nested docs;create_doc/edit_doc/delete_doccreated a doc (real document_id), appended content, and trashed it — all as the user.✅ Markdown rendering: agent replies (bold, lists, tables, code) render as sanitized HTML in the web chat —
markedparses,DOMPurifystrips anything unsafe (agent output is untrusted); streaming re-renders each delta; plain-text fallback if the CDN scripts fail.
Build & deploy notes (learned the hard way)
Runtime image must be ARM64 and built via CodeBuild. A QEMU cross-build on an x86 host produces an image that fails to start on Graviton (500 with no logs). The runtime is built + deployed with the AgentCore CLI (
agentcore configure/agentcore deploy), which runs CodeBuild in the cloud. The agent image includesaws-opentelemetry-distroand runs underopentelemetry-instrument(required for AgentCore log/trace collection).Runtime container logs:
aws logs tail /aws/bedrock-agentcore/runtimes/<runtime_id>-DEFAULT --since 15m.WebSocket endpoint
/wson port 8080 (same app as/ping+/invocations), matching the AgentCore SDK contract — not a separate port.Gateway auth: send the Cognito access token (has the
client_idclaim thatallowedClientsvalidates); an ID token 403s withinsufficient_scope.Errors from the agent are returned as HTTP 200
{error: ...}— AgentCore wraps non-2xx asRuntimeClientErrorand drops the body.
Notes & limitations
Session granularity: one long conversation thread per user (the Memory
session_idis derived fromactor_id). All of a user's turns — webhook, web UI, and reconnects — share one context. There is no "new chat" affordance yet.Memory is STM-only: the Memory resource stores raw turns (30-day retention), no long-term extraction/summarization strategies are configured.
whoamireports thelark:{open_id}, not a display name — the tool only sees what the Gateway interceptor injects (identity from the JWT), and the JWT carries no name claim.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/aws-samples/sample-lark-identity-on-agentcore-interceptor'
If you have feedback or need assistance with the MCP directory API, please join our Discord server