A common answer to "can we put DynamoDB into Claude" is: sure, IAM handles the permissions. It does handle more than most people expect. DynamoDB can restrict a caller to individual items and to individual attributes. What it cannot do is any of that for a chat client shared by a team, and the four reasons are worth knowing before you paste a key into a config file.
The setup everybody starts with
One IAM user, one access key, one MCP server config. The config goes into Claude Desktop or ChatGPT, the key sits in it as an environment variable, and whoever has the file has the key. That is the shape of every local DynamoDB MCP setup, and it is the shape the official AWS servers assume too.
From there the whole permission model is the IAM policy behind that one key. Everybody in the team is that key. So the question is not "what can IAM do", it is "what can IAM do when there is exactly one identity for the whole team, and the caller is a language model that writes its own queries".
What DynamoDB can actually restrict
Credit where it is due: DynamoDB's fine-grained access control is real and it is more than table-level. Three condition keys do the work:
dynamodb:LeadingKeysrestricts a caller to items whose partition key matches a value, typically a substitution variable holding the caller's own user id.dynamodb:Attributesrestricts which top-level attributes a request may touch.dynamodb:Selectforces the request to name the attributes it wants, instead of asking for all of them.
Used together, those three give you the classic multi-tenant policy: a player reads their own scores and nobody else's, and only the columns the game needs. It is a good model. It was designed for an application, and it shows.
Gap 1: the identity the policy needs does not exist
LeadingKeys only means something when the policy can substitute a per-person value into it: ${www.amazon.com:user_id}, ${graph.facebook.com:id}, ${accounts.google.com:sub}. Those come from web identity federation, where every end user assumes a role of their own with their own token.
A static access key in an MCP config has no such variable. There is one principal, and it is the same principal for the support agent, the analyst and the contractor. You can hard-code a tenant id into the policy, which gives you one key per tenant and a config file to distribute per tenant. What you cannot express is "this person, right now", because the thing holding the key never learns who is asking.
Gap 2: Scan is outside the fence
AWS says this in the policy examples, not in the footnotes. The item-level example deliberately omits Scan from the allowed actions, with the reason given plainly: Scan returns all items regardless of the leading keys
.
Which puts you in an awkward position with a chat client. Scan is the tool a model reaches for when it does not know the key, and "how many orders landed yesterday" is a scan unless somebody built an index for it. Allow Scan and your item-level fence is decorative. Deny Scan and half the questions people actually ask come back empty, which is how a permitted path loses to a CSV export.
Gap 3: attribute conditions are checked on the request, not on the response
This one surprises people who have shipped the policy already. dynamodb:Attributes is evaluated against what the request asked for. If the request does not ask for anything specific, there is nothing to evaluate. The documentation states the consequence directly: for read operations without a projection expression, all attributes will be returned regardless of any attribute restrictions in the policy
.
That is why the working examples always pair Attributes with StringEqualsIfExists on dynamodb:Select, forcing SPECIFIC_ATTRIBUTES, and why AWS adds a further warning about ReturnValues on write paths. Get that pairing wrong, and the policy reads as if it protects the e-mail column while the e-mail column comes back on every unprojected read. A model writing its own GetItem call has no reason to send a projection expression.
Gap 4: hiding is not masking
Suppose you get all of it right. The attribute is now either in the response or it is not. There is no setting that returns the last four digits of a card, a hashed e-mail address, or a pseudonym stable enough that the model can group by customer without reading the customer.
For a chat assistant that distinction is the whole job. "How many repeat orders per customer this month" needs a customer identifier that is consistent but not readable. Include the attribute and you have handed out the e-mail address. Exclude it and the question cannot be answered. A binary switch cannot split that, no matter how the policy is written.
What the trail says afterwards
Whatever your CloudTrail configuration, it records the principal that made the call. That principal is the IAM user behind the key. When somebody asks six months later who pulled the customer table on a Tuesday evening, the honest answer is the name of a service account, plus a list of everyone who ever had a copy of the config file.
| What you want | IAM policy on a shared key | Loopthink in front of the same table |
|---|---|---|
| Per-person scope | Needs a federated identity per user; a static key has one principal | Each person signs in with company SSO, the call runs as them |
| Scan under control | Allowed or denied, and allowing it bypasses item-level conditions | Scan is a tool like any other, on per role, capped per call and paged |
| Sensitive attributes | Only if the request names its attributes; otherwise returned in full | Masked on the way out, whatever the model asked for |
| Usable but not readable | Not expressible: an attribute is in or out | Masked in place, so grouping and counting still work |
| Audit | The IAM principal behind the key | The person, the tool, the table, the timestamp |
The servers AWS publishes
Worth knowing where they now stand. The DynamoDB MCP server in the AWS Labs repository is a design tool: data modelling, schema validation, cost estimation. The item-level operations that used to live there moved to the AWS API MCP server, which wraps the CLI and is candid about its own boundaries: NOT designed for multi-tenant environments
, intended for stdio as a local server using a single user's credentials
, with the further caution that some AWS read only operations can still return AWS credentials or sensitive information
.
None of that is a flaw. It is a scoping decision, and it is the right one for a developer on a laptop with their own AWS profile. It is simply not a team deployment, and the README says so before you find out the hard way.
What we do instead
Loopthink holds one read-only key, scoped to the tables you tick, and never hands it to a client. Each person connects Claude or ChatGPT to Loopthink with their own company login, so the call carries an identity the IAM policy never had. A role decides which tools that person gets: get an item by key, query, scan, describe the schema. Sensitive attributes are masked on the way out rather than excluded from the request, so an analyst can count orders per customer without reading a single e-mail address. Every call lands in one audit log under the person's name.
The setup, the role matrix and the exact IAM policy are on the AWS connector page, with the per-client walkthroughs for Claude and ChatGPT.
The other half of an AWS account is what happened to those records, which lives in CloudWatch Logs and has a permission model of its own: who is allowed to read the log line?