MCP server
Worked examples
Eleven real transcripts from Sample mode, each with the question a person asked, the call the agent made, and the answer Dynomatic gave back.
Every request and every answer on this page came out of a running Dynomatic, against the tables Sample mode seeds. Nothing here was written by hand.
Three conventions hold throughout.
- A request block is the
paramsof atools/call, which is what your client builds from the agent’s tool call. Every call here names the sample profile__dynomatic_sample__inus-east-1. - A successful answer arrives twice, once as text and once as the same JSON in
structuredContent. The answer blocks below are that JSON. A refusal is shaped differently. Its message arrives as text and itsrefusalobject arrives instructuredContent, which is the part an agent branches on. - Long answers are trimmed, and the line above each one says what was cut. Attributes are shown in a readable order. No value is changed.
The server was read-only throughout, which is the default. Three of the calls below are writes, and all three are refused rather than run.
Find your way around a profile
“What tables can you see, and what does the Commerce one look like?”
{ "name": "list_profiles", "arguments": {} }
Trimmed to the sample profile. The machine this was captured on returned sixteen more.
{
"profiles": [
{
"type": "dynomatic-managed",
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"mfa": false,
"sso": false,
"credentialProcess": false,
"endpoint": "http://127.0.0.1:56558",
"local": true,
"sample": true
}
]
}
{
"name": "list_tables",
"arguments": { "profile": "__dynomatic_sample__", "region": "us-east-1" }
}
{
"TableNames": [
"Commerce",
"IndexShowcase",
"KnowledgeBase",
"LargeEvents",
"ProjectManagement",
"SessionsWithTtl",
"TypeZoo",
"UniversitySingleTable",
"UsersPostsComments"
]
}
{
"name": "describe_table",
"arguments": { "profile": "__dynomatic_sample__", "region": "us-east-1", "table": "Commerce" }
}
Trimmed. The full answer also carries AttributeDefinitions, DeletionProtectionEnabled, the ARNs, the table id, and the size and throughput numbers.
{
"Table": {
"TableName": "Commerce",
"TableStatus": "ACTIVE",
"KeySchema": [
{ "AttributeName": "PK", "KeyType": "HASH" },
{ "AttributeName": "SK", "KeyType": "RANGE" }
],
"GlobalSecondaryIndexes": [
{
"IndexName": "GSI1",
"KeySchema": [
{ "AttributeName": "GSI1_PK", "KeyType": "HASH" },
{ "AttributeName": "GSI1_SK", "KeyType": "RANGE" }
],
"Projection": { "ProjectionType": "ALL" },
"IndexStatus": "ACTIVE"
},
{
"IndexName": "GSI2",
"KeySchema": [
{ "AttributeName": "GSI2_PK", "KeyType": "HASH" },
{ "AttributeName": "GSI2_SK", "KeyType": "RANGE" }
],
"Projection": { "ProjectionType": "ALL" },
"IndexStatus": "ACTIVE"
}
],
"ItemCount": 122,
"BillingModeSummary": { "BillingMode": "PAY_PER_REQUEST" },
"StreamSpecification": { "StreamEnabled": true, "StreamViewType": "NEW_AND_OLD_IMAGES" }
},
"TimeToLiveDescription": { "TimeToLiveStatus": "DISABLED", "AttributeName": "" },
"embeddings": []
}
This is the opening move for every other example. The profile value is a name Dynomatic owns, so an agent passes it back exactly as returned and never gets to describe an account another way. describe_table is what tells the agent that GSI1 exists at all, which is the difference between the next example and a full scan.
Tools used: list_profiles, list_tables and describe_table.
Query on a key condition, and scan when there is no key
“What has customer cust-001 ordered this year?”
GSI1 keys orders by customer, so this is a Query with a key condition and no filter at all.
{
"name": "query",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "Commerce",
"indexName": "GSI1",
"keyConditionExpression": "GSI1_PK = :customer AND begins_with(GSI1_SK, :prefix)",
"expressionAttributeValues": {
":customer": { "S": "CUSTOMER#cust-001" },
":prefix": { "S": "ORDER#2026" }
},
"projectionExpression": "PK, GSI1_SK, #status, #total",
"expressionAttributeNames": { "#status": "status", "#total": "total" }
}
}
{
"Items": [
{
"PK": { "S": "ORDER#order-1001" },
"GSI1_SK": { "S": "ORDER#2026-04-07T14:59:34.000Z" },
"status": { "S": "delivered" },
"total": { "N": "269.99" }
},
{
"PK": { "S": "ORDER#order-1006" },
"GSI1_SK": { "S": "ORDER#2026-05-10T13:34:28.000Z" },
"status": { "S": "delivered" },
"total": { "N": "161.99" }
},
{
"PK": { "S": "ORDER#order-1021" },
"GSI1_SK": { "S": "ORDER#2026-06-23T10:42:59.000Z" },
"status": { "S": "shipped" },
"total": { "N": "140.39" }
}
],
"Count": 3,
"ScannedCount": 3
}
“And which customers are on the premium tier?”
tier is on no key and no index, so there is nothing to build a key condition out of.
{
"name": "scan",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "Commerce",
"filterExpression": "tier = :tier",
"expressionAttributeValues": { ":tier": { "S": "premium" } },
"projectionExpression": "customerId, #name, tier, lifetimeSpend",
"expressionAttributeNames": { "#name": "name" },
"limit": 50
}
}
{
"Items": [
{
"customerId": { "S": "cust-001" },
"name": { "S": "Olivia Bennett" },
"tier": { "S": "premium" },
"lifetimeSpend": { "N": "612.96" }
},
{
"customerId": { "S": "cust-005" },
"name": { "S": "Hannah Lindqvist" },
"tier": { "S": "premium" },
"lifetimeSpend": { "N": "1728.99" }
},
{
"customerId": { "S": "cust-012" },
"name": { "S": "Amara Diallo" },
"tier": { "S": "premium" },
"lifetimeSpend": { "N": "918.98" }
}
],
"Count": 3,
"ScannedCount": 50,
"LastEvaluatedKey": { "PK": { "S": "ORDER#order-1015" }, "SK": { "S": "ORDER#order-1015" } }
}
Both answers hold three items. The query read three items to find them and the scan read fifty, which is what ScannedCount is telling you, and on a real table that number is the bill. The scan also stopped at its limit rather than at the end of the table, so LastEvaluatedKey is there to pass back as cursor for the next page. Reach for scan when no key condition exists, not as a default.
Tools used: query and scan.
Read one item you already have the key for
“Show me order-1006 in full.”
{
"name": "get_item",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "Commerce",
"key": { "PK": { "S": "ORDER#order-1006" }, "SK": { "S": "ORDER#order-1006" } }
}
}
Trimmed. The full item also carries _ct, _md, currency, itemCount, shippingFee, subtotal, tax, trackingNumber, shippedAt and deliveredAt.
{
"Item": {
"PK": { "S": "ORDER#order-1006" },
"SK": { "S": "ORDER#order-1006" },
"_et": { "S": "Order" },
"orderId": { "S": "order-1006" },
"customerId": { "S": "cust-001" },
"status": { "S": "delivered" },
"total": { "N": "161.99" },
"orderedAt": { "S": "2026-05-10T13:34:28.000Z" },
"paymentMethod": { "M": { "type": { "S": "card" }, "last4": { "S": "4242" } } },
"shippingAddress": {
"M": {
"line1": { "S": "1184 Pine St" },
"city": { "S": "Seattle" },
"state": { "S": "WA" },
"postalCode": { "S": "98101" },
"country": { "S": "US" }
}
},
"GSI1_PK": { "S": "CUSTOMER#cust-001" },
"GSI1_SK": { "S": "ORDER#2026-05-10T13:34:28.000Z" },
"GSI2_PK": { "S": "ORDERSTATUS#delivered" },
"GSI2_SK": { "S": "2026-05-10T13:34:28.000Z" }
}
}
The previous example projected four attributes to keep the answer small. When the agent then needs the whole item, get_item on the key it already has is one read and cannot accidentally widen into a scan. A key that matches nothing comes back with no Item rather than an error.
Tools used: get_item.
Ask a question in PartiQL
“Which customers have spent more than 900 dollars?”
{
"name": "partiql",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"statement": "SELECT customerId, \"name\", tier, lifetimeSpend FROM \"Commerce\" WHERE _et = 'Customer' AND lifetimeSpend > 900"
}
}
{
"Items": [
{
"customerId": { "S": "cust-005" },
"name": { "S": "Hannah Lindqvist" },
"tier": { "S": "premium" },
"lifetimeSpend": { "N": "1728.99" }
},
{
"customerId": { "S": "cust-007" },
"name": { "S": "Grace Kim" },
"tier": { "S": "plus" },
"lifetimeSpend": { "N": "1088" }
},
{
"customerId": { "S": "cust-012" },
"name": { "S": "Amara Diallo" },
"tier": { "S": "premium" },
"lifetimeSpend": { "N": "918.98" }
}
]
}
A SELECT is a read, so it runs while the server is read-only. INSERT, UPDATE and DELETE are writes and go through the production check on the table the statement itself names, and any other leading keyword is refused rather than guessed at. Note what is missing from the answer. PartiQL returns no LastEvaluatedKey, so this tool has no continuation to hand back, which is why the 4 MB result cap matters more here than on query.
Tools used: partiql.
Ask what Dynomatic already knows about a table
“You have been reading this table. What have you learned about it?”
{
"name": "schema_profile",
"arguments": { "profile": "__dynomatic_sample__", "region": "us-east-1", "table": "Commerce" }
}
Trimmed to two of the 45 attributes, with the null fields on each left out. The full answer runs to about 11 KB.
{
"tableName": "Commerce",
"registryKey": "__dynomatic_sample__:us-east-1:Commerce",
"environment": "staging",
"pk": { "name": "PK", "typ": "S" },
"sk": { "name": "SK", "typ": "S" },
"gsis": [
{
"indexName": "GSI1",
"pkName": "GSI1_PK",
"pkType": "S",
"skName": "GSI1_SK",
"skType": "S",
"projectionType": "ALL"
},
{
"indexName": "GSI2",
"pkName": "GSI2_PK",
"pkType": "S",
"skName": "GSI2_SK",
"skType": "S",
"projectionType": "ALL"
}
],
"itemsObserved": 18,
"totalAttributes": 45,
"attributes": [
{
"path": "customerId",
"primaryType": "S",
"frequency": 0.7777777777777778,
"pattern": "Enum",
"enumValues": ["cust-001", "cust-002", "cust-003", "cust-005", "cust-012"],
"entityScopes": ["Customer", "Order"]
},
{
"path": "PK",
"primaryType": "S",
"frequency": 0.5,
"pattern": "CompositeKey",
"entityScopes": ["Customer", "Order"]
}
],
"entities": [
{ "label": "Customer", "pkPrefix": null, "skPrefix": null, "observationCount": 4, "attributeCount": 15 },
{ "label": "Order", "pkPrefix": "ORDER#", "skPrefix": null, "observationCount": 5, "attributeCount": 24 }
],
"siblings": [],
"embeddings": []
}
Read itemsObserved before anything else. The table holds 122 items and Dynomatic had seen 18 of them in this session, so every frequency here is over those 18 and not over the table. This is a report of what has passed through the app, which is also why a table nothing has read yet errors instead of answering.
The stored data model is a separate thing, and asking for it does not build it.
{
"name": "data_model",
"arguments": { "profile": "__dynomatic_sample__", "region": "us-east-1", "table": "Commerce" }
}
{
"registryKey": "__dynomatic_sample__:us-east-1:Commerce",
"dataModel": null
}
null means this table has never been inferred, not that inference found nothing. infer_data_model is the fix and it is the Re-infer action, so it writes to Dynomatic’s own data and needs Allow writes. On a read-only server it is refused, which is the second of the three refusals below.
Tools used: schema_profile, data_model and infer_data_model.
Ask what the items say
“Which articles discuss API tokens?”
KnowledgeBase holds about 200 help-center articles. No key or index answers a question about what an article is about, and a filter on the title would only match the word. describe_table says whether there is a better option. This is the embeddings array from the same call as the first example, run against KnowledgeBase.
{
"embeddings": [
{
"attribute": "embedding",
"model": "text-embedding-3-small",
"provider": "http-openai",
"dimensions": 64,
"indexName": "embedding-cosine-index",
"partitionAttribute": "category"
}
]
}
That entry names a partitionAttribute, so the index searches one value of category at a time. An agent that misses it is told so rather than getting a narrower answer than it asked for.
{
"name": "semantic_search",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "KnowledgeBase",
"text": "which articles discuss API tokens"
}
}
{
"refusal": {
"kind": "NotSearchable",
"reason": "PartitionRequired",
"message": "The vector index on table KnowledgeBase (__dynomatic_sample__ in us-east-1) partitions on \"category\", and SearchVectors searches one partition at a time.",
"fix": "Pass `partition` with the category to search within, in DynamoDB JSON, for example {\"S\": \"...\"}. The distinct values of `category` come from a scan projecting that attribute, or from the table's data model; search each one that matters.",
"at": { "profile": "__dynomatic_sample__", "region": "us-east-1", "table": "KnowledgeBase" },
"attribute": "category",
"attributes": []
}
}
A scan projecting category returns six distinct values. Authentication is the one that holds token articles.
{
"name": "semantic_search",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "KnowledgeBase",
"text": "which articles discuss API tokens",
"partition": { "S": "Authentication" },
"topK": 4
}
}
Trimmed. Each hit also carries the article’s tags, and the answer ends with a consumed block reporting vectorSearchRequestBytes.
{
"hits": [
{
"item": {
"pk": { "S": "ARTICLE#token-expiry-policies" },
"sk": { "S": "v1" },
"category": { "S": "Authentication" },
"title": { "S": "Setting a maximum token lifetime" }
},
"score": 0.40692085986959914
},
{
"item": {
"pk": { "S": "ARTICLE#revoking-tokens" },
"sk": { "S": "v1" },
"category": { "S": "Authentication" },
"title": { "S": "Revoking a compromised token" }
},
"score": 0.4343965282283625
},
{
"item": {
"pk": { "S": "ARTICLE#api-token-introspection" },
"sk": { "S": "v1" },
"category": { "S": "Authentication" },
"title": { "S": "Checking what a token can do" }
},
"score": 0.4543155681836556
},
{
"item": {
"pk": { "S": "ARTICLE#api-authentication-header" },
"sk": { "S": "v1" },
"category": { "S": "Authentication" },
"title": { "S": "Authenticating API requests" }
},
"score": 0.5008644268297056
}
],
"scoreDirection": "lowerIsCloser",
"scoreDirectionAssumed": false,
"index": {
"name": "embedding-cosine-index",
"attribute": "embedding",
"dimensions": 64,
"distance": "COSINE"
},
"model": { "provider": "http-openai", "id": "text-embedding-3-small", "dimensions": 64 }
}
None of those four titles contains the phrase the question used, which is the point. Read scoreDirection before ranking anything yourself, because this index scores distance and a lower number is a closer match. The projection decides what comes back, so these hits carry title, category and tags and not the article body. Fetch that with get_item on the pk and sk a hit returns.
Tools used: semantic_search, and describe_table for the embeddings entry. Embeddings covers how a table gets vectors in the first place.
Three refusals, and what each one asks for
“Set that customer back to the plus tier.”
{
"name": "put_item",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "Commerce",
"item": {
"PK": { "S": "CUSTOMER#cust-001" },
"SK": { "S": "CUSTOMER#cust-001" },
"tier": { "S": "plus" }
}
}
}
{
"refusal": {
"kind": "WritesDisabled",
"message": "This call would perform a write to AWS and the MCP server is read-only.",
"fix": "Turn on Allow writes under Settings > MCP server in Dynomatic.",
"effect": "remoteWrite"
}
}
Nothing reached AWS. Dynomatic classified the call before running it and refused it on that classification, so the item is untouched. effect is the classification.
“Then work out the data model for Commerce.”
{
"name": "infer_data_model",
"arguments": { "profile": "__dynomatic_sample__", "region": "us-east-1", "table": "Commerce" }
}
{
"refusal": {
"kind": "WritesDisabled",
"message": "This call would perform a write to Dynomatic's own data and the MCP server is read-only.",
"fix": "Turn on Allow writes under Settings > MCP server in Dynomatic.",
"effect": "localWrite"
}
}
Same kind, different effect. This one never leaves your machine, and at the Allow writes rung it runs without any production check, because nothing in your account changes.
“Search the posts table the way you searched the articles.”
{
"name": "semantic_search",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "UsersPostsComments",
"text": "which articles discuss API tokens"
}
}
{
"refusal": {
"kind": "NotSearchable",
"reason": "NoRecipe",
"message": "table UsersPostsComments (__dynomatic_sample__ in us-east-1) has no embedding recipe, so nothing on it is searchable by meaning.",
"fix": "Create an embedding recipe under Table settings > Embeddings in Dynomatic and run its backfill. Until then, read this table with query or scan.",
"at": { "profile": "__dynomatic_sample__", "region": "us-east-1", "table": "UsersPostsComments" },
"attribute": null,
"attributes": []
}
}
No switch fixes this one. The table has no vectors, so the fix is to make some or to read it another way, and the refusal says both. Branch on kind, and on reason where a kind has more than one, rather than matching on the prose.
Permissions and refusals has the full table of kinds.
Start an export, or find out why you cannot
“Write the whole Commerce table out to a file.”
{
"name": "export_start",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "Commerce",
"destination": "/tmp/commerce.jsonl",
"format": "jsonl"
}
}
{
"refusal": {
"kind": "WritesDisabled",
"message": "This call would perform a write to Dynomatic's own data and the MCP server is read-only.",
"fix": "Turn on Allow writes under Settings > MCP server in Dynomatic.",
"effect": "localWrite"
}
}
An export reads DynamoDB and writes a file on your machine, so it is a local write and needs Allow writes. It never consults the production check, which means a table you would not let an agent write a row to can still be exported at that rung.
At Allow writes this answers with a jobId instead, and export_status reports on it. An id that no job answers to gets the same answer as one that has aged out.
{ "name": "export_status", "arguments": { "jobId": "export-does-not-exist" } }
{
"refusal": {
"kind": "UnknownJob",
"message": "Dynomatic has no job \"export-does-not-exist\".",
"fix": "Use the jobId export_start returned. A finished job is kept for an hour and then dropped; start a new export.",
"id": "export-does-not-exist"
}
}
A finished job answers for an hour. After that, and after a client reconnects onto a new session, the id stops resolving while the export itself is unaffected and still listed in the Transfers panel. Poll on a timer rather than in a loop, because a session is held to 120 calls a minute.
Tools used: export_start and export_status. Exports and show in the app covers the running and done phases in full.
Put a table in front of the person
“Open those orders so I can look at them myself.”
{
"name": "show_table",
"arguments": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "Commerce",
"indexName": "GSI1",
"keyConditionExpression": "GSI1_PK = :customer AND begins_with(GSI1_SK, :prefix)",
"expressionAttributeValues": {
":customer": { "S": "CUSTOMER#cust-001" },
":prefix": { "S": "ORDER#2026" }
}
}
}
{
"shown": {
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "Commerce",
"command": {
"commandType": "query",
"IndexName": "GSI1",
"KeyConditionExpression": "GSI1_PK = :customer AND begins_with(GSI1_SK, :prefix)",
"ExpressionAttributeValues": {
":customer": { "S": "CUSTOMER#cust-001" },
":prefix": { "S": "ORDER#2026" }
}
}
}
}
A tab opened on Commerce with that query filled into the query builder, and nothing ran. The answer is an echo of what was seeded, not a result set, and there are no items in it because Dynomatic did not read any. You read the query and press Run.
That is deliberate. An agent’s expression crosses four layers of Dynomatic’s own model on the way to the builder, and every condition one of them drops makes the read wider than the one asked for. So the tab waits for you. A query the builder cannot hold exactly opens the table with nothing filled in and says so.
Because it reads nothing and changes nothing, this works while the server is read-only, which is when an agent most wants to show you something.
Tools used: show_table.
Reach something that has no typed tool
“What is the embedding recipe on KnowledgeBase built from?”
No typed tool answers that. list_commands is how an agent finds out whether one of Dynomatic’s own commands does, and contains narrows the list from 259 rows to the relevant ones.
{
"name": "list_commands",
"arguments": { "contains": "embedding", "runnableOnly": true }
}
Trimmed to three of the ten matches. The answer also carries matched: 10, total: 259 and a nextCursor of null.
{
"commands": [
{
"name": "embedding_backfill_status",
"class": "read",
"effect": "read",
"gates": ["vectorSearch"],
"shape": "Headless",
"runnable": true,
"args": ["registryKey", "targetAttribute"],
"target": "none"
},
{
"name": "embedding_recipe_list",
"class": "read",
"effect": "read",
"gates": ["vectorSearch"],
"shape": "Headless",
"runnable": true,
"args": ["registryKey"],
"target": "none"
},
{
"name": "embedding_recipe_save",
"class": "localWrite",
"effect": "localWrite",
"gates": ["vectorSearch"],
"shape": "Headless",
"runnable": true,
"args": ["recipe"],
"target": "none"
}
]
}
args gives the exact argument names, in the camelCase spelling the wire uses. An argument a command does not declare is refused outright rather than ignored.
{
"name": "invoke_command",
"arguments": {
"name": "embedding_recipe_list",
"args": { "registryKey": "__dynomatic_sample__:us-east-1:KnowledgeBase" }
}
}
{
"result": [
{
"registryKey": "__dynomatic_sample__:us-east-1:KnowledgeBase",
"targetAttribute": "embedding",
"hashAttribute": null,
"providerId": "aip_4w9sL6DgGG",
"providerKind": "http-openai",
"modelId": "text-embedding-3-small",
"dimensions": 64,
"observedDimensions": 64,
"sources": [{ "entityId": null, "entityLabel": null, "template": "${[ title ]}\n\n${[ body ]}" }],
"dryRunAt": "2026-09-16T16:26:35.731752Z",
"createdAt": "2026-09-16T13:59:23.121141Z",
"updatedAt": "2026-09-16T13:59:23.121141Z"
}
]
}
So the vectors were built from each article’s title and body, which is why Ask what the items say matched on meaning rather than on the title alone. Note that runnable: true is not permission. It says the command’s shape can cross the protocol at all, and the ladder is checked separately, so a local write like embedding_recipe_save still needs Allow writes.
Tools used: list_commands and invoke_command. Any command, through one tool covers what is withheld and why.
Find out which tables an agent may write to
“Before you change anything, which of these is production?”
{ "name": "environment_report", "arguments": {} }
Trimmed to the sample profile and its two classified tables. The machine this was captured on returned 45 profiles and 1,373 tables.
{
"profiles": [
{
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"classification": {
"environment": "development",
"confidence": 1.0,
"signals": [],
"source": "auto"
},
"mixed": true
}
],
"tables": [
{
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "Commerce",
"classification": {
"environment": "staging",
"confidence": 1.0,
"signals": [],
"source": "auto"
}
},
{
"profile": "__dynomatic_sample__",
"region": "us-east-1",
"table": "UniversitySingleTable",
"classification": {
"environment": "production",
"confidence": 1.0,
"signals": [],
"source": "auto"
}
}
]
}
This one report answers the question for every table at once. Commerce is staging, so Allow writes admits a write to it. UniversitySingleTable is production, so that one needs Allow production writes as well. The seven sample tables with no row of their own fall back to the profile, and the profile is mixed, so a write to one of those is refused as unverified until somebody classifies it.
source says who decided, auto for the classification engine and user-override for a person who set it by hand. Every row here is auto. An agent cannot write either kind, because a production verdict is the evidence its own writes are judged on, so the commands that set one are withheld from the protocol.
Tools used: environment_report. Environment classification covers how a verdict is reached, and Permissions and refusals covers what each rung admits.
Where to go next
- Tools reference lists all 35 tools with their effects and gates.
- Permissions and refusals has every refusal kind and the switch each one wants.
- Troubleshooting starts with the most common symptom, which is a client that lists no tools.
Last updated on