-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
sequenceDiagram
participant C as MCP client
participant S as dataverse-mcp
participant E as Entra ID
participant D as Dataverse Web API
C->>S: tool call over stdio (JSON args)
S->>S: Pydantic validation + URL normalise + whitelist
S->>S: token cache lookup (per scope)
S-->>E: acquire token (cache miss only)
S->>D: HTTPS request, Bearer token
D-->>S: OData JSON (retry on 429 / gateway 5xx)
S-->>C: JSON string on stdout
The server is a subprocess of your client. stdout carries the MCP protocol and nothing else; all logging goes to stderr, which is why the code never calls print().
Validation happens before anything leaves the machine. Each tool's arguments are parsed into a Pydantic model with extra='forbid', and dataverse_url is normalised and whitelist-checked inside that model — so a bad URL or an unapproved host is rejected without a token ever being minted, and surfaces as an MCP tool error rather than the JSON error contract below.
Tokens are cached per scope in the server process and reused until 5 minutes before expiry. A cache miss runs the blocking credential call on a worker thread under a per-scope lock, so ten concurrent tool calls on a cold cache produce one sign-in, not ten. That wait is capped by DATAVERSE_AUTH_TIMEOUT_SECONDS.
All HTTP goes through one shared httpx.AsyncClient (max 20 connections, 10 keep-alive; 10 s connect, 60 s read/write, 120 s for $batch).
Every tool returns a string containing JSON — never Markdown, never a raised exception.
List shape
{ "records": [ ... ], "count": 25, "has_more": false }The collection key names the thing being listed (records, tables, solutions, environments, …). count is how many are in this response.
Single shape — a flat object of the fields that tool documents, e.g. dataverse_whoami returning UserId / BusinessUnitId / OrganizationId.
Error shape
{ "error": true, "message": "Dataverse returned HTTP 403: [<odata-code>] <Dataverse's own message>" }is_transient: true is added when the request timed out before the server answered — the operation may still have completed, so verify before retrying.
| Condition | Message |
|---|---|
| Dataverse returned an error status | Dataverse returned HTTP <code>: [<odata-code>] <message> |
| Request timed out |
The request timed out before the server responded. The operation may still have completed on the server; verify before retrying. + is_transient
|
| Host unreachable | Could not reach <host>: <detail> |
| Credential failed | Authentication failed. Run az login to refresh your Azure CLI session, or check DATAVERSE_AUTH_TYPE and your credential configuration. |
| Anything unhandled | Unexpected error in <tool_name>. See the server logs for details. |
The last one is deliberate: exception text can carry internal paths and hostnames, so the detail stays in the server log. Dataverse error messages are passed through but capped at 2,000 characters, suffixed … (truncated).
List tools take top, defaulting to 50 on most of them. The server asks Dataverse for pages of min(top, 500) via Prefer: odata.maxpagesize and follows @odata.nextLink until it has top records — so top is a total, not a page size, and one tool call may be several HTTP round-trips.
has_more is true when the response came back full (count >= top). That means it can be true when the next page would in fact be empty: treat it as "ask for more if you need more", not as proof more exists. dataverse_execute_fetchxml is the exception — it pages with a Dataverse paging cookie, so its has_more is exact and the cookie is returned alongside it.
A few tools that wrap Dataverse functions with no server-side paging fetch the whole set and trim it locally — those report both count (returned) and total_count (the real size). dataverse_get_role_privileges is the one to remember: a System Administrator role carries roughly 4,100 privileges and about 1 MB, so it trims to top and tells you the true magnitude in total_count.
| Size | Behaviour |
|---|---|
| Over 1 MB | Logged as a warning; response returned normally |
| Over 5 MB | Response replaced with an error |
{ "error": true, "message": "Response too large (7.3 MB). Narrow the query with select/top/filter." }The fix is always the same: name the columns in select, lower top, add a filter. Some tools have their own lever — include_content, include_solutions, max_chars — which is cheaper than narrowing the query.
Up to 3 attempts per request.
| Condition | Retried for | Wait |
|---|---|---|
| HTTP 429 (service protection) | All methods |
Retry-After seconds, capped at 30 s; 2 s if the header is missing or unparseable |
| HTTP 502 / 503 / 504 | GET, PUT, DELETE only | 1 s, 2 s, 4 s |
| Connection failure | GET only | 1 s, 2 s, 4 s — then DataverseConnectionError
|
| Timeout | GET only | 1 s, 2 s, 4 s — then re-raised |
Any other status is returned to the caller immediately.
POST and PATCH are not retried on 5xx. A gateway error can arrive after Dataverse committed the write, so a retry would create a second record or re-apply an update. The 5xx is returned instead and the agent is told to verify. 429 is different — it is a rejection before processing, so nothing was committed and retrying every method is safe.
| Behaviour | Why it bites |
|---|---|
| Forms, views and apps read back the published version | A read after an edit looks unchanged. Use dataverse_retrieve_unpublished (savedqueries, systemforms, appmodules, webresourceset — not sitemap), or run dataverse_publish_customizations first. |
dataverse_get_total_record_counts is snapshot-based |
Counts come from a snapshot taken at most once every 24 hours, and are all-or-nothing — one unrecognised logical name fails the whole batch with HTTP 400. All zeros usually means the snapshot job has not run. Use dataverse_count_records for an exact, live count. |
dataverse_get_setting returns HTTP 200 for a name that does not exist |
Dataverse answers with a null detail rather than an error. The tool reports setting_found: false, which never collapses with a setting genuinely holding "", "false" or 0. |
dataverse_validate_fetchxml returns HTTP 200 for an invalid query |
An unknown table or attribute comes back as an error-severity message, not an error status. Read has_errors / error_count, not the status. |
dataverse_retrieve_access_origin returns HTTP 200 when there is no access |
No access and a nonexistent record are both successful calls; the answer is in the prose of access_origin. |
dataverse-mcp 3.9.1 — Repository · PyPI · Issues · Changelog · MIT
Get started
Configure
Tools
- Tool-Index
- Tools-Core
- Tools-Schema
- Tools-Solutions
- Tools-Plugins
- Tools-Security
- Tools-Custom-APIs
- Tools-Apps
- Tools-Variables
- Tools-Flows
- Tools-Views
- Tools-Forms
- Tools-Connections
- Tools-Web-Resources
- Tools-Jobs
Understand