Skip to content

Releases

v0.9.2

Choose a tag to compare

@rededis rededis released this 10 Nov 20:33
2d65728

Changed

  • Published from GitHub Actions through npm trusted publishing (OIDC) instead of a stored npm token. The package and its provenance are unchanged (#121).

v0.9.1

Choose a tag to compare

@rededis rededis released this 29 Oct 20:00
0f57dc5

Added

  • create_entity takes has_notes and has_activities, to enable notes and attachments or activities on the new table. Neither can be turned off once enabled (#109).

Fixed

  • create_entity says what it created when it fails part way. A timeout on the table request says the table may not exist, may exist in part or may still be in progress, that no additional column was sent, and to wait up to 2 minutes before checking with get_entity_schema; a timeout on a column request says the same of that column. A failed column request says that the table was created, which columns were created, which one failed and why, and which were not sent; so does a table whose id did not come back. Before, these surfaced as the bare timeout or Dataverse error (#113).
  • create_entity failed on every call: Dataverse requires HasNotes in CreateEntity and refused the request without it (Required field 'HasNotes' is missing). The tool now sends HasNotes, false by default; HasActivities was already sent as false (#109).

Server 0.1.0-rc.2

Server 0.1.0-rc.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Oct 16:12
9bdf88f

Added

  • The MCP server for Microsoft Dataverse over Streamable HTTP (MCP 2026-07-28), for remote agents: the metadata-read, data-read, data-write, actions and functions tool groups, without the development tools (#76).
  • Access with bearer tokens listed by their SHA-256 in a JSON config file (#76).
  • Roles in the config file: which tool groups a token may use, which entity sets it may create, update or delete in, which actions and functions it may call, and which Dataverse user it acts on behalf of (#77).
  • The image: one esbuild bundle on distroless Node 24, non-root (uid 65532), for linux/amd64 and linux/arm64, with the licence texts of the bundled packages (#79, #96).
  • An audit log on stdout: one JSON line ("type":"audit") per tool call, with the token, role, actAs, the JSON-RPC id, the proxy's X-Request-Id, the names the call used, how it ended, the record count and the Dataverse requests, throttling and retries it caused; and a line per refused token, refused Origin and call to a tool the role does not have. It records no content: no filters, bodies, parameter values, records or Dataverse error text (ADR-0001 §12, #78).
  • DATAVERSE_AUDIT_KEY (optional, at least 32 characters): the key of the HMAC in the fingerprint the audit log writes for a value refused for its shape. Without it the fingerprint has only a length and flags (#78).

Changed

  • The startup and shutdown messages are written to stderr; stdout carries only audit lines (#78).

Server 0.1.0-rc.1

Server 0.1.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Oct 17:56
0ff879b

Added

  • The MCP server for Microsoft Dataverse over Streamable HTTP (MCP 2026-07-28), for remote agents: the metadata-read, data-read, data-write, actions and functions tool groups, without the development tools (#76).
  • Access with bearer tokens listed by their SHA-256 in a JSON config file (#76).
  • Roles in the config file: which tool groups a token may use, which entity sets it may create, update or delete in, which actions and functions it may call, and which Dataverse user it acts on behalf of (#77).
  • The image: one esbuild bundle on distroless Node 24, non-root (uid 65532), for linux/amd64 and linux/arm64, with the licence texts of the bundled packages (#79, #96).

v0.9.0 — MCP TypeScript SDK v2, Node 20

Choose a tag to compare

@rededis rededis released this 02 Oct 18:52
3d5fa3f

Changed

  • The server is built on MCP TypeScript SDK v2 (@modelcontextprotocol/server 2.2) instead of @modelcontextprotocol/sdk 1.x (closes #75).

    • BREAKING — Node 20 or later is required; the SDK does not run on Node 18.
    • The stdio server now speaks MCP 2026-07-28 as well as the 2025 revisions. A client that opens with server/discover is served on 2026-07-28; one that opens with initialize gets the revision it asks for, as before. Claude Code 2.1.287 was observed opening with server/discover, so it now runs on the new revision instead of falling back.
    • Tool names, descriptions, order and input schemas are the same, with three differences on every tool, none of which changes what a tool accepts: $schema is https://json-schema.org/draft/2020-12/schema instead of draft-07; $schema is the second key of the schema instead of the first; and the tool member "execution":{"taskSupport":"forbidden"} is no longer sent (forbidden is the default).
    • Input that fails a tool's schema is reported with different wording: one line, without the MCP error -32602: prefix.
    • A call to a tool name the server does not have is answered with a JSON-RPC error (-32602) instead of a tool result marked isError.
    • When stdin closes, the server stops at once: a request still in progress is not answered. Before, it was allowed to finish and its result was written. Clients that keep stdin open for the session, as MCP clients do, are unaffected. A one-shot pipe (printf '…' | dataverse-mcp-server) gets no answer to a call that had not finished, and a write sent that way may still reach Dataverse.
    • On a 2026-07-28 session every result also carries resultType and _meta with the server's name and version, and tools/list carries ttlMs and cacheScope.

    An error thrown inside a tool still reaches the client as the tool's error text with isError: true.

v0.8.0 — Dataverse service protection limits and request timeouts

Choose a tag to compare

@rededis rededis released this 02 Oct 11:29
9bd51eb

Added

  • DATAVERSE_REQUEST_TIMEOUT_MS (default 30000, maximum 120000, matching Dataverse's own 2-minute limit on a message operation): every Dataverse request and the token request to Microsoft Entra ID now time out instead of hanging a tool call indefinitely. The error says which request timed out. A value that is not a whole number from 1 to 120000 is reported through dataverse_setup like a missing variable, rather than silently replaced by the default (#73).

  • Handling of Dataverse service protection limits (closes #74). A 429 Too Many Requests used to fail the tool call with the raw Dataverse error. Now:

    • A throttled request is resent after the wait its Retry-After header asks for, up to DATAVERSE_MAX_ATTEMPTS sends in total (default 3). Without a usable header the server backs off 2 s, then 4 s, and so on. While it waits, no other request is sent, because Dataverse extends the wait for a client that keeps sending.
    • If the waits of one request would add up to more than DATAVERSE_MAX_RETRY_WAIT_MS (default 15000), or the attempts run out, the tool call fails with "Dataverse is busy: a service protection limit was reached. Retry in N s." The raw Dataverse 429 text is no longer shown.
    • At most DATAVERSE_MAX_CONCURRENCY requests are in flight (default 8). Up to DATAVERSE_MAX_QUEUE_LENGTH more (default 100) wait their turn in arrival order for at most DATAVERSE_MAX_QUEUE_WAIT_MS (default 10000); beyond either bound the call fails with a "Dataverse is busy … Retry later" error instead of queuing indefinitely.
    • All five variables are optional. An out-of-range value is reported through dataverse_setup rather than replaced by the default.

    Behaviour to know about: writes are retried after a 429 like reads, as Microsoft's own clients do. A 429 holds other requests back for the wait Dataverse asked for, but no longer than DATAVERSE_MAX_RETRY_WAIT_MS (closes #88): in a live test Dataverse answered light requests straight after a 429 with Retry-After: 300, so a longer pause would only have been downtime of our own making. With the defaults the limits add at most 25 s of waiting to a request, chosen so that one request at the default timeout still fits the 60 s a client built on the MCP TypeScript SDK waits by default; the README gives the full arithmetic.

Changed

  • Tools are registered by purpose instead of by topic (closes #72): metadata-read, data-read, data-write, actions, functions and development, as named in ADR-0001 §4. Nothing a client sees changes except order: tool names, descriptions, input schemas and DATAVERSE_ALLOW_DELETE behaviour are identical, pinned by a tools/list snapshot taken before the change. tools/list now returns tools grouped this way rather than in the old per-file order.
  • Tool input schemas are built once at module load instead of on every registration, ahead of SDK v2 building a server instance per HTTP request.
  • When the cached token has expired, concurrent tool calls share one token request instead of each sending their own (#73).
  • The client reports failures as typed errors (DataverseApiError with status and the Dataverse error code, DataverseTimeoutError, DataverseNetworkError, DataverseAuthError), and tools map "not found" by status rather than by matching message text. API error messages read as before. A network failure now says which request failed and why, instead of a bare fetch failed (#73).
  • HTTP execution sits behind a RequestExecutor interface, the seam for throttling handling (#74), acting on behalf of a user (#77) and audit logging (#78) (#73).

Fixed

  • The server no longer writes a non-protocol line to stdout at startup. When a .env file was loaded, dotenv 17 printed a banner (◇ injected env (7) from .env // tip: …) with console.log, and on stdio stdout is the MCP protocol stream. Clients built on the MCP TypeScript SDK reported a parse error for that line and carried on; a stricter client could refuse the connection. Present in every release since 0.1.0.
  • get_picklist_options, add_attribute and create_entity no longer report "Global OptionSet not found" for a server error whose body happens to contain "404". They matched 404 anywhere in the message; they now check the status (#73).

v0.7.1 — list_entities prefix filtering

Choose a tag to compare

@rededis rededis released this 17 Aug 11:23
56872f1

Fixed

  • list_entities no longer fails with HTTP 501 when a prefix filter is used without a solution filter (closes #66) — the DATAVERSE_ENTITY_PREFIX-only setup that .env.example documents. Metadata entities reject startswith outright (0x8006088a: The "startswith" function isn't supported for Metadata Entities), not merely in combination with or as the code assumed, so the prefix is now applied client-side on every path rather than on one of the two.

    Cost of the fix, stated plainly: a prefixed call now fetches all table definitions and filters them here. Against a real org that is ~2.1 MB transferred to return 23 rows. Dataverse offers no server-side prefix match on metadata, and the solution-filtered path already worked this way; what is returned to the caller is unchanged.

    The test that should have caught this asserted the generated $filter string against a mocked client, so it agreed with the code while neither agreed with Dataverse. It now asserts that no startswith reaches the server and that the prefix is honoured on the returned rows.

  • get_entity_schema reports a choice column whose OptionSet did not come back, instead of silently omitting its option_set. An omitted summary is indistinguishable from a non-choice column — an answer, and the wrong one. Such columns now appear in the same warning block as a failed cast lookup.

v0.7.0 — bind Choice columns to a Global OptionSet

Choose a tag to compare

@rededis rededis released this 17 Aug 09:51

Added

  • add_attribute and create_entity accept global_option_set on a Picklist attribute, binding the column to an existing Global OptionSet by name instead of creating a private copy of the values (closes #60). Mutually exclusive with options; an unknown name fails as Global OptionSet not found: '<name>'.

    Global OptionSets exist so one choice list can be shared across columns and tables. Previously the only ways to get a shared list were to accept a local copy that silently drifts the first time anyone edits one of them, or to create the column by hand in the maker portal — which breaks an otherwise scripted schema workflow.

    create_entity resolves names and builds every attribute body before creating the table, so nothing a client-side check can reject — an unknown set name, a Picklist with no values, an impossible DateTime pairing — can leave a half-built table behind; Dataverse offers no transaction to roll one back. A name repeated across attributes costs one lookup, not one per column.

Changed

  • Reworded the tool descriptions for get_attribute_dependencies, delete_attribute, update_attribute and get_entity_schema. These are the always-loaded prompt text a model reads to decide whether to call a tool, so they are pruned rather than expanded: return shapes and component-type enumerations that a caller gets for free by calling are gone, each meaning now lives in one tool rather than being restated across several, and the rename/type-change recipe sits in update_attribute alone. No behavior change.

Notes

Three Web API details established live against a real org, all of which contradict the obvious reading:

  • The binding goes through the GlobalOptionSet navigation property, not an inline OptionSet. Sending an inline one with IsGlobal: true is rejected: 0x80048403 — Only Local option set can be created through the attribute create. IsGlobal flag must be set to 'false'.
  • The binding target must be the MetadataId. Microsoft's documentation states the alternate key by name — GlobalOptionSetDefinitions(Name='...') — works there too, but a real org answers HTTP 500: Guid should contain 32 digits with 4 dashes. Hence the name is resolved to a GUID first.
  • Sending an inline OptionSet and a binding together is not an error. Dataverse silently drops the binding and creates a local copy, which is why the pair is rejected client-side rather than left to the platform.

v0.6.0 — OptionSet identity in read tools

Choose a tag to compare

@rededis rededis released this 17 Aug 06:58
3161f06

Changed

  • BREAKING — get_picklist_options no longer returns a bare [{ value, label }] array. It now returns { option_set: { name, is_global, metadata_id }, options: [{ value, label }] } (closes #61). The flat array made a Local OptionSet and a Global one indistinguishable: the response shape was identical either way, and matching values do not prove a binding. Callers that consumed the array directly need to read .options.
  • get_picklist_options now resolves Choice, Status, State and MultiSelect columns, not just Choice. statecode / statuscode previously failed with "Picklist attribute not found"; the not-found message now names the whole choice family.

Added

  • get_entity_schema attaches an option_set summary — { name, is_global, metadata_id, option_count } — to every choice-style column, so a schema dump answers "what can this column contain, and is the list shared?" without a call per column. The option values themselves are deliberately excluded; read them per column with get_picklist_options.

Failure behavior

Reading OptionSet data turns get_entity_schema from one metadata request into five, so the two tools handle a failing request differently — in both cases so that an unknown answer is never presented as a definite one:

  • get_entity_schema degrades. The base attribute list is the tool's contract and does not depend on OptionSets, so a failing lookup no longer costs the caller the column list. The partial coverage is reported in a second content block (the JSON stays in the first), because a silently missing option_set would read as "this column has no options". A failure of the base request itself still fails the call.
  • get_picklist_options fails hard. There, an empty result means "not a choice column", so swallowing an error would turn a transient failure into a confident Choice attribute not found on a column that does exist. For the same reason a matched column whose OptionSet did not come back is an error rather than a default of is_global: false.

Notes

Verified live against a real org: a column bound to a global set reports is_global: true with the global set's own metadata_id, while a locally-defined column reports is_global: false with an auto-generated name.

Two Web API details found during that verification, both encoded in src/tools/optionset-utils.ts:

  • OptionSet is only reachable through a type cast, and the cast must name a concrete type. Casting to the abstract base — /Attributes/Microsoft.Dynamics.CRM.EnumAttributeMetadata — is rejected with HTTP 500 0x8006088a: Unexpected attribute type, despite the docs listing it as a GET-able entity type. Each concrete choice type is therefore requested separately.
  • The sibling GlobalOptionSet navigation property is not a usable binding signal: for a locally-defined column it resolves to that column's own local set rather than returning null, so it cannot tell the two cases apart. IsGlobal on the expanded OptionSet is the reliable indicator.

v0.5.0 — invoke_action / invoke_function

Choose a tag to compare

@rededis rededis released this 16 Jun 14:52
30db77f

Added

  • Two new tools for invoking Dataverse Web API actions and functions, covering operations that fall outside CRUD (closes #57):
    • invoke_action(name, entity_set?, id?, parameters?) — POSTs a bound or unbound action. Unbound → POST /<name> (e.g. UnpublishDuplicateRule with { DuplicateRuleId }); bound → POST /<entity_set>(<id>)/Microsoft.Dynamics.CRM.<name> (e.g. PublishDuplicateRule bound to duplicaterule, QualifyLead bound to lead). parameters is sent as the JSON request body. Bound-vs-unbound is per the Web API $metadata, not the SDK shape — verified live against a real org.
    • invoke_function(name, entity_set?, id?, parameters?) — GETs a bound or unbound function (e.g. WhoAmI). parameters are inlined as OData function arguments using parameter aliases; strings are quoted, GUIDs/numbers/booleans passed as-is.
    • Bare operation names are namespaced automatically for bound calls (Microsoft.Dynamics.CRM.<name>); a fully-qualified name is passed through. Operation names are validated against a dotted-identifier pattern and bound id must be a GUID, so a caller cannot inject extra path/query segments. Bound calls require both entity_set and id; unbound calls require neither (the half-specified case is rejected).

Use case

Unblocks CRM operations that are only exposed as actions — notably publishing duplicate-detection rules (a rule created via create_record lands unpublished and has no effect until PublishDuplicateRule) and lead qualification (QualifyLead). Surfaced while building the fundaicapital Lead→Account qualification flow (DP-129 / DP-135).

Note

invoke_action can perform arbitrary mutating operations and is intentionally ungated for now; capability-based access control (safe-by-default gating of writes/actions) is tracked separately in #45 / #46. invoke_function is read-only.