MCP server
Permissions and refusals
The read-only, writes, and production-writes ladder, what production means, and every refusal an agent can be handed.
Dynomatic decides every call from two things: what the call does to your data, and what you have allowed. Nothing else, and nothing an agent can send, changes the answer.
What a call does
Every tool and every command is classified before a target exists.
- A read reads DynamoDB or reads Dynomatic’s own data. It works at every rung.
- A local write changes Dynomatic’s own database and never reaches AWS. Saving a query, storing an inferred data model, importing a schema and starting an export are local writes. The target’s environment does not constrain them, because nothing in your account changes.
- A remote write reaches your AWS account.
The ladder
Read-only, the default, admits reads and refuses both kinds of write.
Allow writes admits local writes, and admits remote writes to a target the environment classifier calls staging or development.
Allow production writes admits remote writes to every target, including the ones classified production and the ones with no classification.
Writes-off wins. If you turn Allow writes off while Allow production writes is still on, the server is read-only. Your production choice is kept, not cleared, so turning writes back on restores it.
The ladder is read fresh on every call, so a switch you flip applies to the next call an agent makes, including a call inside a script that is already running.
What production means here
The environment of a target is whatever the classification engine says, and an agent cannot change it. The commands that write a classification are withheld from the protocol outright, because a production verdict is the evidence a write is judged on.
A remote write under Allow writes is refused on two kinds of target.
- A table, or a profile, classified Production.
- A target Dynomatic could not verify. That covers a target with no classification row at all, and a table with no row of its own inside a profile that mixes environments.
An unverified target is refused as firmly as a production one, and that is deliberate. An unknown environment is not evidence that a table is safe to write to. Turning on Allow production writes is you saying “all of them”, including the ones nobody has labeled.
The environment_report tool returns every profile and table Dynomatic has classified with its environment and confidence, so an agent can tell you in advance which writes will go through and ask you to classify a table rather than retrying against a wall.
Scripts check every table they touch
run_script clears the ladder for the table you name, and then clears it again for every table the script’s own DynamoDB calls name, on every call. Nothing stops a script naming a second table on its next line, so the clearance the tool granted is not the clearance those calls run under. A refused call fails inside the script the way an AWS error would.
Naming no table checks the whole profile, which a profile holding both production and non-production tables fails, so naming the table you mean is the way to get a narrower answer.
PartiQL is classified by its statement
SELECT is a read and works while the server is read-only. INSERT, UPDATE and DELETE are writes and go through the production check over the table the statement itself names. Anything else is refused rather than guessed at, because calling a CREATE a read would run it at the read-only rung.
The statement is rendered before it is classified, so a ${[ … ]} template resolves first and the verdict covers the table the variable resolves to. A table argument naming a different table from the statement is refused.
Every refusal an agent can get
A refused call is a normal result marked as an error, carrying a refusal object with the kind, what happened, and what to change. An agent branches on kind rather than reading prose.
| Kind | What happened | What fixes it |
|---|---|---|
WritesDisabled |
The call writes and the server is read-only | Turn on Allow writes |
ProductionRefused |
The target is classified production and production writes are off | Turn on Allow production writes, or target a non-production table |
TargetUnverified |
The target’s environment could not be established | Classify it in Dynomatic, or turn on Allow production writes |
Gated |
The tool needs a plan feature this Dynomatic does not have | Upgrade the plan, or use Sample mode |
UnknownProfile |
No profile has that name. The answer lists the names that exist | Call list_profiles and use one of its names exactly |
UnknownCommand |
No command has that name. The answer lists the nearest ones | Call list_commands and use one of its names exactly |
NotHeadless |
The command cannot run over the protocol. A reason says which kind of cannot | Depends on the reason, below |
NotSearchable |
semantic_search cannot search that table the way the call asked. A reason says which of six ways, and attribute or attributes names what it was about |
Depends on the reason, below |
AiConsentRequired |
The call would send your text to an embedding provider, and you have not agreed to send this account’s text to that provider. provider names which one |
Run one Vector search or embedding dry run in Dynomatic and answer Send item text to AI?. Only you can answer it |
UnsupportedStatement |
The PartiQL statement’s leading keyword is none of the four | Use SELECT, INSERT, UPDATE or DELETE |
OwnershipFailed |
Dynomatic could not verify which profile owns the workspace at startup, so local writes are off for the session | Restart Dynomatic with the AWS config readable |
Busy |
Something Dynomatic runs one at a time is already running, such as a script | Wait for it or cancel it, then retry |
NoSession |
The call starts work that outlives it and the connection carries no session to own it | Initialize a session and send its id. Most clients do this already |
UnknownJob |
No job answers to that id | Use the id export_start returned. A finished job is dropped after an hour |
TooManyJobs |
The background job registry is full of running work | Cancel a running job or wait for one to finish |
TooLarge |
The request carries more items than the per-call ceiling | Split it into smaller batches |
RateLimited |
Too many calls from this session. The answer says how many milliseconds to wait | Wait that long and retry |
Timeout |
The call did not finish inside 60 seconds | Answer any prompt Dynomatic is showing, then retry |
NotSearchable and AiConsentRequired both come from semantic_search and from nothing else, so a client that never searches by meaning never sees either one. Three refusals, and what each one asks for has a real NotSearchable beside both shapes of WritesDisabled.
The NotHeadless reasons
NotHeadless carries one of six reasons, because “this command cannot run here” has six different fixes.
NeedsWebviewdrives the Dynomatic window or your clipboard.NeedsChannelstreams its progress into the window and has nowhere to send it. Background exports are these, and they have their own tools instead.NeedsDialogopens a file dialog and waits for a person.Withheldmeans somebody read the command and decided an agent must not run it. Nothing an agent can change.Unclassifiedmeans nobody has decided yet, so it refuses rather than guessing.NoWindowmeans the command would run but Dynomatic has no window open to show the result in.
Any command, through one tool covers what is withheld and why.
The NotSearchable reasons
NotSearchable carries one of six reasons, and each one names what the next call needs.
NoRecipemeans the table embeds nothing at all. Create a recipe under Table settings, then Embeddings, and run its backfill, or read the table withqueryorscaninstead.AttributeRequiredmeans the table embeds more than one attribute, soattributeslists them and the call has to passattributeas one of them.UnknownAttributemeans theattributethe call passed is not one this table embeds.attributeslists the ones it does.NoIndexmeans the namedattributeis embedded but the table carries no vector index over it, so the vectors exist with nothing to search them by.PartitionRequiredmeans the index partitions onattributeandSearchVectorssearches one partition at a time, so the call has to passpartitionwith a value of it.PartitionRejectedmeans the index partitions on nothing, so there is no partition to narrow to. Retry withoutpartition.
A reason is the part to branch on. The kind alone does not say whether the fix is a call the agent can correct on its own, as AttributeRequired and PartitionRequired are, or something only you can do in the app.
What a write changes in the window
An agent’s write goes down the same path your own does, so it obeys the same plan limits, resolves the same template variables and uses the same credentials. What a write does to an open grid covers which tabs refresh and which do not.
Last updated on