Skip to content

Releases: sassoftware/sas-mcp-server

v1.19.0 — SAS Intelligent Decisioning end to end, and Microsoft Copilot

Choose a tag to compare

@Criptic Criptic released this 23 Oct 14:50
e500fd4

A minor release. Before, the SAS Intelligent Decisioning tools covered rule sets and decisions built only from rule sets. They now cover the whole product: every decision step kind, lookup tables, segmentation trees, value lists, code files and custom functions. Every asset is stored in a folder. Decisioning is split into two tiers, and the server reaches Microsoft 365 Copilot and Copilot Studio. (#69, #68)

Highlights

🧩 Decisions with every step kind

create_decision_flow / update_decision_flow take steps of every kind, nested freely: rule set, segmentation tree, code file, sub-decision, model, assignment, condition, and range, equals or like branches. The server wires each step: it picks the newest locked revision, maps terms by name and adds a decision variable for every unmapped term, so the decision validates and publishes. It refuses combinations the service would accept but could not run. The tool description has a worked example and spells out the rules that are easy to get wrong.

📖 Lookup tables

get_lookup_table, create_lookup_table, update_lookup_table and activate_lookup_table manage the keys and values that rules consult.

  • Versions: entries are edited in versions, and an active version is never changed.
  • Activation: it reports active, running or failed, with Viya's reason, and also handles rollback and republish.
  • Already-published decisions: they use newly activated entries without being published again (verified live).
  • Rules: rule tools resolve a lookup table named in a lookup condition or a lookupValue action to its ID. They report conditions and actions Viya would store as broken in problems.
  • Delete guard: deleting a lookup table that rule sets still use is refused, and the refusal names those rule sets. Viya itself would delete it and leave them unable to generate code.

🌳 Trees, value lists, code files and custom functions

These are segmentation trees (including create_segmentation_tree_from_table, which turns a FICO-style strategy table into one tree), value lists, DS2/Python/SQL code files and custom functions. Custom functions are compiled before they are saved. One shared life cycle covers every kind: list, get, copy and delete_decisioning_asset, plus get_decisioning_asset_code, list_decisioning_asset_revisions and lock_decisioning_asset_revision. validate_decision_expression checks a DS2 condition or action before it goes into a rule set or decision.

🗂️ Two decisioning tiers

  • Tier 7, Decisioning Foundation (34 tools): everything it had, plus lookup tables, the life-cycle tools and expression validation.
  • Tier 10, Decisioning Advanced (10 tools, new): trees, value lists, code files and custom functions.
  • Tier 10 needs Tier 7, which puts that content into decisions and reads, locks and deletes it. The server logs a warning at startup when Tier 10 is enabled without it.

📁 Every decisioning asset is stored in a folder

Rule sets and decision flows used to be created outside any folder, so no folder view showed them. Every decisioning create now takes folder, which can be:

  • a path such as /Public/Content/Decisions
  • My Folder, or a path below it
  • a folder ID
  • a folder URI

Without folder, the create uses the new MCP_DEFAULT_FOLDER. With neither, the call is refused before anything is created and the model is told to ask the user. Updates never move an asset, and an asset created earlier is filed by its next update. The new find_folder tool (Tier 2, read-only) resolves the same references.

🚀 Publishing for MAS

publish_decision_flow now generates the decision's code for MAS, as the Intelligent Decisioning UI does. Before, decisions that use a lookup table could not be published, and now they can. Modules keep the decision's own input names, without the UI's trailing underscores.

In a 50-case comparison against the modules published before, every output matched except PathID, which MAS code leaves empty. score_data now also accepts plain input names for modules published from the UI, and reports the mapping in inputs_renamed.

🪟 Microsoft 365 Copilot and Copilot Studio

  • examples/m365-copilot/: a Microsoft 365 Copilot declarative agent.
  • examples/copilot-studio/: a Copilot Studio how-to.
  • examples/copilot-studio/certified-connector/: a draft package for Microsoft's MCP certification.
  • examples/microsoft-copilot.md: a guide to choosing between the two routes.

Two new settings make the integrations work:

  • MCP_OAUTH_CLIENT_SECRETS=true lets Microsoft 365 Copilot register itself again.
  • MCP_TOOL_RENAME=catalog_=infocat_ works around Copilot Studio's runtime, which fails on tool names that start with catalog_.

🔢 Tool count

115 tools in 11 tiers (was 92 in 10 tiers): 57 read-only and 28 destructive. Tier 7 goes from 22 to 34 tools, the new Tier 10 has 10, and Tier 2 gains find_folder.

🐳 Container image

ghcr.io/sassoftware/sas-mcp-server:1.19.0

Also published as 1.19, 1 and latest. Helm chart 0.6.0 (appVersion 1.19.0) is a minor chart bump for three new values: server.defaultFolder, server.toolRename and server.oauthClientSecrets.

⚠️ Upgrading

  • Decisioning creates need a folder. Set MCP_DEFAULT_FOLDER (chart: server.defaultFolder) or pass folder on each call. Without either, create_business_ruleset and create_decision_flow are refused.
  • Trees, value lists, code files and custom functions are in the new Tier 10. A deployment that limits MCP_TIERS adds 10, alongside 7, to get them.
  • publish_decision_flow now publishes MAS code. Outputs are unchanged except PathID, which is now empty.

📄 Details

v1.18.0 — User ID in the server log and configurable client timeout

Choose a tag to compare

@Criptic Criptic released this 29 Sep 05:20
c3307f4

A minor release. The server log now names the SAS Viya user behind every tool call, and the timeout of every Viya REST call is configurable. (#65)

Highlights

👤 The Viya user of every tool call, in the server log

A shared HTTP deployment serves many people through one process, and its log said which tool ran but not for whom, so a field report could not be matched to the person who hit it. Each tool call now logs one line:

Tool call: execute_sas_code (user: sasdemo)

The id comes from GET /identities/users/@currentUser, asked with the caller's own token, so it is exactly the identity Viya authorizes the call against. For a client-credentials token, it is the client's id.

  • One lookup per token, not per call. Results are cached by a hash of the token, never the token itself.
  • Never in the way. The lookup has its own 10-second timeout. A failure logs user: unknown, is retried after five minutes, and never fails the tool.
  • Both transports. It hooks in where each tool obtains its token, so HTTP and stdio both log it, and stdio never starts a second sign-in.
  • Server log only. The opt-in collection log is unchanged.

⏱️ VIYA_CLIENT_TIMEOUT

The timeout of every Viya REST call was fixed at 300 seconds. It is now VIYA_CLIENT_TIMEOUT (chart: server.clientTimeout), with 300 as the default. Lower it to fail fast behind a proxy that drops idle connections; raise it for compute contexts that are slow to start. A value that is not a number greater than 0 stops the server at startup with the reason.

🔢 Tool count

92 tools, unchanged: 51 read-only and 21 destructive.

🐳 Container image

ghcr.io/sassoftware/sas-mcp-server:1.18.0

Also published as 1.18, 1 and latest. Helm chart 0.5.0 (appVersion 1.18.0), a minor chart bump for the new server.clientTimeout value.

⚠️ Upgrading

  • One extra request per sign-in. Each new token makes a single GET /identities/users/@currentUser. It needs no permission beyond a signed-in user.
  • The server log grows by one line per tool call. If you ship server logs somewhere, the user id now appears in them.

📄 Details

v1.17.0 — HTTP Debug Environment Variables

Choose a tag to compare

@Criptic Criptic released this 25 Sep 14:16
d5c07a5

A minor release. HTTP_DEBUG=true writes every request this server sends to the SAS Viya APIs, and each response, to a separate file, so a failing tool call can be traced to the exact request behind it. (#64)

Highlights

🔍 HTTP_DEBUG traces every SAS Viya API request

A tool failure reaches the person as the model's summary of an error. The request that caused it — a wrong path, a missing media type, a body of the wrong shape — is nowhere to be seen, and one tool call can make several requests: paging, polling a compute job, fetching its log.

With HTTP_DEBUG=true in .env, each request sent through the server's Viya client and its response are appended as JSON lines to HTTP_DEBUG_LOG_PATH (default ~/.sas-mcp-server/http-debug.log), never to the server log, so stdio's protocol stream is untouched. Two records per exchange share an id: a request (method, URL, headers, body) and a response (status, elapsed_ms, headers, body). A request with no matching response failed in transit; a body read that failed midway is a response with an error, and the caller still gets that error.

What the trace keeps out and what it keeps in:

  • Redacted with the collection log's rules: Authorization, cookies and other credential-shaped headers, query parameters and body keys, plus inline Bearer and JWT strings. Not redacted: data values. Table rows and SAS code appear in bodies, so review the file before sharing it.
  • Bounded. Bodies are capped at HTTP_DEBUG_MAX_BODY_BYTES (default 4096; 0 records none), binary bodies are described rather than recorded, multipart uploads are not buffered, bodies over 1 MiB are clipped without being decoded or parsed whole, and the file rotates (HTTP_DEBUG_MAX_LOG_BYTES, HTTP_DEBUG_LOG_BACKUPS).
  • Faithful. A URL is recorded exactly as sent unless a parameter had to be masked, so it can be pasted into curl to reproduce the call.
  • Off the event loop. Records are written from a worker thread, as the collection log's are.

Off by default. The server warns at startup while it is on; it is meant for troubleshooting, not for a shared deployment. An unusable path turns tracing off with a warning rather than stopping the server. OAuth sign-in and token-refresh calls are not traced.

🔢 Tool count

92 tools, unchanged: 51 read-only and 21 destructive.

🐳 Container image

ghcr.io/sassoftware/sas-mcp-server:1.17.0

Also published as 1.17, 1 and latest. Helm chart 0.4.2 (appVersion 1.17.0); no chart values changed. To trace a deployment, pass HTTP_DEBUG through the chart's extraEnv.

📄 Details

v1.16.1 — Fix for new Claude Desktop behavior that led to unnecessary report creations

Choose a tag to compare

@Criptic Criptic released this 24 Sep 13:52
8d20e1b

A patch release. apply_report_operations edits a report in place unless told otherwise: save_as is now the only switch to a new report. (#63)

Highlights

🛠️ apply_report_operations edits in place unless save_as is set

Until now the mode was inferred: any result_report_name or result_folder turned the call into a save-as clone. A client layer that must send every parameter cannot leave a name out, and a model asked to fill a required slot invents one, so every edit through such a layer quietly became a new report — the shape of a September field report against Claude Desktop 2.110, where a "change the title" request left the original untouched and a copy behind.

Now the flag alone decides:

  • save_as=false (the default) edits the report in place, whatever else is passed. A result_report_name or result_folder given alongside it is refused with a message that says which to choose, never acted on. Blank values count as not given, and the earlier rejection of blank names is gone with the ambiguity that justified it.
  • save_as=true applies the operations to a new report named by result_report_name and/or placed by result_folder; at least one is required, and the source is left untouched.

Every result now carries mode: in_place or save_as, so a caller can see which happened without reading the rest.

🔢 Tool count

92 tools, unchanged: 51 read-only and 21 destructive. apply_report_operations gains the save_as parameter.

🐳 Container image

ghcr.io/sassoftware/sas-mcp-server:1.16.1

Also published as 1.16, 1 and latest. Helm chart 0.4.1 (appVersion 1.16.1); no chart values changed.

⚠️ Upgrading

  • A bare result_report_name no longer clones. A caller that relied on it to save the result as a new report must add save_as=true; until it does, the call is refused with a message that says so, and nothing is created or edited.

📄 Details

v1.16.0 — ODS HTML Results

Choose a tag to compare

@Criptic Criptic released this 24 Sep 11:58
4d96fc2

A minor release. execute_sas_code now returns the HTML results as SAS Data and AI Studio would show them, graphs included; the interactive views keep rendering on current Claude hosts; and one host path that broke on Anthropic's side is documented as a known issue. (#62)

Highlights

📊 execute_sas_code returns the HTML results

The compute context opens no HTML destination of its own — SAS Data and AI Studio's page comes from ODS statements it adds to every submit — so a job submitted through the API yielded a listing and nothing else, and a graph could not be seen at all.

The code now runs between an ods html5 statement and its close. Before the close comes ;*';*";*/;run;quit;, which ends whatever the code left open — an unterminated statement, quote or comment, or a step without run; — so the close still runs; none of it appears in the log the model reads. When the code printed something, the page is saved to the Viya Files service and the result gains html_results_url: opening it asks for a SAS Viya sign-in, and the file sits in no folder, so only the person who ran the code (and administrators) can open it. html_results_error reports a page that could not be saved without failing the call; html_results=false submits the code exactly as given.

The page — 40 to 70 KB, mostly the ODS stylesheet — never enters the model's context; only the link does. In the SAS log view it is the new Results tab, shown first when the code ran cleanly, with an "Open in browser" link.

Two details learned against a live Viya: the guard is run;quit;, not the quit;run; of SAS Enterprise Guide, because quit; inside an unfinished DATA step is an error on a line the caller never wrote and once left the session unable to run the next job; and when the guard's run; ends a step the code left open, the log splits the echo of the closing line around the step's notes, so the wrapper is stripped by line number, not by text.

🪟 Views on current Claude hosts

Three changes, each forced by a host:

  • The view binding is repeated where hosts look for it — on the tools/call result, in both spellings the extension has used, and as _meta.ui.csp on the resources/read item — not only on the tools/list entry.
  • View URIs carry a fingerprint of the page (ui://sas-viya/sas-log/execute_sas_code.<hash>.html, from the ui package's files, the version and the tier and read-only selection), because hosts cache a page by its URI — claude.ai even across a connector being removed and added again — so a changed page was served stale. The plain URI is served too, for hosts holding an older tool listing.
  • MCP_STATELESS_HTTP (chart: server.statelessHttp) serves requests without an MCP session. The 2026-07-28 protocol revision has no session and opens with server/discover, which the default stateful transport refuses; claude.ai's custom-connector backend speaks only that revision. Verified end to end: the views, Results tab included, render in Claude Desktop through a custom connector.

The README's "Interactive views" section now has a table of clients and how they must be connected.

⚠️ Known issue: Claude Desktop and local servers

Since 22 September 2026, Claude Desktop 2.2553 no longer renders views from a server configured in claude_desktop_config.json (which accepts stdio servers only): Desktop passes a local server's tools to the chat through a bridge that rebuilds each tool's metadata and drops the view binding, so the chat never asks for the page. The tools work as before and execute_sas_code still returns its HTML results link. Reported as anthropics/claude-ai-mcp#1069. Until it is fixed, the views render when the HTTP server is added to Claude as a custom connector with MCP_STATELESS_HTTP=true.

🏷️ Product names

SAS Studio is SAS Data and AI Studio and SAS Information Catalog is SAS Data Governance, throughout tool descriptions, docs and tests. API paths and tool names are unchanged.

🔢 Tool count

92 tools, unchanged: 51 read-only and 21 destructive. execute_sas_code gains the html_results parameter.

🐳 Container image

ghcr.io/sassoftware/sas-mcp-server:1.16.0

Also published as 1.16, 1 and latest. Helm chart 0.4.0 (appVersion 1.16.0), a minor chart bump for the new server.statelessHttp value.

⚠️ Upgrading

  • execute_sas_code wraps the code by default. Every call now runs between an ods html5 open and close with the guard above, and a call that prints something stores one HTML page in the Viya Files service, kept until deleted. Set html_results=false on a call to submit the code exactly as given. Nothing else in the result changes.
  • View URIs changed shape (…/<tool>.<hash>.html). Hosts pick the new URI up from tools/list; the old plain URI keeps working as an alias.
  • Stateless HTTP is off by default. Turn it on (MCP_STATELESS_HTTP=true, chart server.statelessHttp) for a deployment that will be added to Claude as a custom connector.

📄 Details

v1.15.0 — interactive views for data, SAS logs and the glossary

Choose a tag to compare

@Criptic Criptic released this 15 Sep 11:00
a75528c

A minor release, and the first in which a result can be more than text. Ten tools now carry an interactive view — a data grid, a SAS log viewer and three glossary views — that MCP Apps clients render beside the result. What the tools return is unchanged. (#61)

Highlights

🪟 Interactive views (MCP Apps)

Some results read badly as text however well the tool shapes them: a hundred rows of a table, a SAS log, a form's worth of typed attributes. Clients that support the MCP Apps extension now render a small HTML view in a sandboxed frame beside the result.

View Tools What you get
Data grid query_data, get_castable_data, get_compute_table_data Sort, filter and page. Paging calls the tool again from the view, so a person walks a large table while the model holds only the first page.
SAS log execute_sas_code, get_job_log, submit_batch_job ERROR / WARNING / NOTE lines coloured and counted, jump to the next problem, problems-only and hide-notes filters, the listing on its own tab. A batch job is polled from the view until it ends, so the model spends no turns polling.
Glossary term editor get_glossary_term_type, get_glossary_term A form generated from the term type: a real checkbox for a boolean, a date picker, a multi-select. The attribute wire-format mistakes fixed in v1.14.0 cannot be made from here at all. A draft can be published.
Glossary browser list_glossary_terms The whole glossary as a tree beside a detail pane: search, type and has-assets filters, breadcrumbs, attributes, assigned assets and a sunburst of the hierarchy.
Glossary import import_glossary_terms With the new dry_run=true, the rows exactly as they would be sent, with an Import button.

Every view opens fullscreen from a ⤢ button in its header wherever the host offers that mode. That is where a wide table or a long log is meant to be read.

What keeps this safe:

  • Purely additive. Every tool returns exactly what it did. A client without the extension (Claude Code, a script, an older client) sees no difference.
  • The browser never talks to Viya. Every request a view makes travels through the host to this server as an ordinary tool call, carrying the session's token.
  • A view offers nothing the deployment withheld. A tool removed by MCP_TIERS or MCP_READ_ONLY has no view, and no control inside another view calls it. In read-only mode the glossary browser opens without its Publish button, rather than showing one that can only fail.
  • Fully inlined. The official @modelcontextprotocol/ext-apps bridge (1.7.5, Apache-2.0) is vendored and inlined. Hosts build the frame's Content-Security-Policy from what a server declares, and Claude.ai ignores that declaration, so only inline pages render everywhere. The same choice makes the views work on an air-gapped Viya. No new Python dependency; the image grows by about a third of a megabyte.

Verified in Claude Desktop, including fullscreen on every view. The other clients listed in the README support the extension but have not yet been tested against this server.

📋 get_compute_table_data: rows from a SAS library table

This is the compute-tier counterpart of get_castable_data, which only ever covered CAS. It returns a page of rows from libref.table through the compute session, with values formatted as SAS displays them, the total row count and a truncated flag. It is read-only and runs in the same session as execute_sas_code, so a WORK table from an earlier call can be browsed.

🔎 import_glossary_terms(dry_run=true)

A dry run validates the batch, resolves every parent into its path and orders the rows exactly as the import would, then returns them without sending anything. A real import runs as a job that can fail partway, after earlier rows are already committed, so a preview a person can confirm is worth the extra call.

🏷️ The server says which version it is

The first line of the log, in both transports, now reads:

sas-mcp-server 1.15.0 (fastmcp 4.0.0, stdio) - connecting to SAS Viya at https://…

Until now nothing at startup named the version. The MCP handshake also reported FastMCP's version instead of ours, because FastMCP(...) was never given one, so a client asked what it was connected to answered 4.0.0. The FastMCP version stays in the log line on purpose: both sign-in regressions after the 4.0 upgrade (#54, #58) were specific to it.

🔢 Tool count

92 tools, up from 91: 51 read-only (up from 50, since get_compute_table_data is read-only) and 21 destructive.

🐳 Container image

ghcr.io/sassoftware/sas-mcp-server:1.15.0

Also published as 1.15, 1 and latest. Helm chart 0.3.0 (appVersion 1.15.0), a minor chart bump for the new server.apps value.

⚠️ Upgrading

  • Views are on by default. Set MCP_APPS=false (Helm: server.apps: false) to register none. The tools are unaffected either way.
  • The startup log line changed. It used to read Connecting to SAS Viya at …; it now begins sas-mcp-server <version> (…) - connecting to SAS Viya at …. Anything that matched the old line, such as a log-based readiness check or alert, needs updating.

📄 Details

v1.14.2 — Copilot CLI sign-in, and failures that explain themselves

Choose a tag to compare

@Criptic Criptic released this 11 Sep 06:49
2ed136a

A patch release driven by three field reports. One fixes interactive sign-in from GitHub Copilot CLI; the other two fix failures that happened for a real reason while the output said nothing useful about it.

Highlights

🔐 GitHub Copilot CLI can sign in (#58)

copilot mcp auth failed before SAS Viya was ever contacted, with Redirect URI 'http://127.0.0.1:<port>/' does not match CIMD redirect_uris. An MCP client opens a fresh loopback port for every sign-in, and FastMCP 4.0 enables CIMD (Client ID Metadata Document) by default: a client whose client_id is an HTTPS URL — Copilot CLI is one — has its callback checked against the fixed redirect URIs in its own published document, with no exemption for a varying loopback port. The server now sets enable_cimd=False, which puts those clients on the ordinary dynamic-registration path every other MCP client already uses, and which does let loopback ports vary (RFC 8252 §7.3).

The trade, stated plainly: the server no longer advertises client_id_metadata_document_supported, and private_key_jwt drops out of the advertised token-endpoint auth methods. Both are accurate — with CIMD off it supports neither.

🧾 execute_sas_code says why a job failed (#59, fixes #55)

A refused job submission surfaced as Error calling tool 'execute_sas_code': 'id' — a KeyError leaking out, because the submit never checked its HTTP status and parsed Viya's error body as though it were a job. Viya's actual reason was thrown away, so a model driving the tool retried simpler and simpler SAS code against a failure that had nothing to do with the code. It now reports what Viya said.

The same sweep found two quieter defects in the same path:

  • A log that could not be read came back as an empty log rather than an error — for audit-style work, silently losing the deliverable.
  • A compute session dying mid-job left the tool polling forever, every two seconds, because the error body was read as a job state that was never terminal. Deliberately no wall-clock cap was added alongside the fix: legitimate SAS jobs can run for hours.

🗄️ A caslib 409 points at the data source, not your permissions (#59, fixes #56)

casManagement answers a caslib read with HTTP 409 when CAS cannot connect to the library's underlying data source — verified against postgres (password authentication failed) and Oracle (ORA-00257: archiver error) caslibs, both arriving as the same 409. On its own the status reads as an authorization problem, so the natural next step is to check caslib permissions, find them correct, and be stuck. list_castables, list_source_tables, get_castable_info and get_castable_columns now keep Viya's own reason and add what it means, with the one step that settles it independently of MCP: confirm the caslib opens directly in SAS Viya.

🔢 Tool count

91 tools (50 read-only, 21 destructive) — unchanged from v1.14.1. This release changes error handling and authentication configuration only.

🐳 Container image

ghcr.io/sassoftware/sas-mcp-server:1.14.2

Also published as 1.14, 1 and latest. Helm chart 0.2.6 (appVersion 1.14.2).

📄 Details

Thanks to @bteleuca for diagnosing and fixing the Copilot CLI sign-in (#58), and to @nishah2005 for the reports behind #55 and #56.

v1.14.0 — Business Glossary, corrected by the field

Choose a tag to compare

@Criptic Criptic released this 07 Sep 10:54
31762f0

Highlights

Tier 9 shipped in v1.13.0 and was immediately used to build a real 122-term glossary. This release is what that use found — thirteen fixes and four new tools, all from two rounds of field feedback against the same live deployment. (#52)

The Business Glossary service publishes no OpenAPI document, so every wire format below was established by probing a running Viya. That is the theme of the release: the API accepts a value, stores something else, and says nothing.

🐛 Values that were accepted and silently stored wrong

Three of these produce no error at all — which makes them the ones worth reading.

What you sent What was stored Now
attributes={"Approved": True} the term type's default — never your value a JSON boolean, stored as sent
attributes={"Regions": ["EMEA", "APAC"]} rejected: The value "['EMEA', 'APAC']" … is invalid EMEA,APAC — the one form the glossary takes
a time attribute of 09:15:00+02:00 09:15:00Z — two hours out, no warning 07:15:00Z
an imported row's definition in the description; the definition held the term's own name both columns written from their own field

That last one is the sharpest. The importer's Description column is the description; Definition is a separate system column, and with no Definition column at all the service fills the field in with the term's name. A 122-term import therefore came back with every definition reading like a label and every description holding the text meant for the definition — the field a reader actually sees, wrong on every row, on every term.

✍️ import_glossary_terms — a whole hierarchy in one call

Creating terms one at a time costs a call per term and a wait between levels, because a child needs its parent's id from the previous response. The reported 122-term hierarchy took 122 sequential calls.

This wraps the glossary's own bulk import, so parents are named rather than threaded by id:

[{"name": "Exposure",     "term_type": "Business Term"},
 {"name": "Net Exposure", "term_type": "Business Term", "parent": "Exposure"},
 {"name": "Counterparty", "term_type": "Business Term", "parent": "Exposure\\Net Exposure"}]

Rows may be given in any order and are sorted so every parent precedes its children. Attribute values keep the same forms as create_glossary_term and are validated per term type before the CSV is built, so a bad value fails locally rather than as one line of a job log.

The result names the term each row became. terms carries {name, path, term_id, existed} per row, resolved in one filtered request per 40 distinct names — sibling names are unique, so a (parentId, name) index over the candidates rebuilds the hierarchy and every row's path walks down it. On a 122-term import that is about four requests, not 122.

The same pass fixes what created was hiding. A row naming a term that already exists at that path is not created, but the job counts it successful either way — so a re-import of a 122-term hierarchy reported 122 created and changed nothing. The job's own creationTimeStamp separates the two: a term older than the job was already there. new and already_existed now report the split, and existed says which is which per row.

🧱 Term types can be designed, not only read

create_glossary_term_type, update_glossary_term_type and delete_glossary_term_type complete the tier. A glossary could previously be read and populated through these tools but never designed through them, so any deployment wanting a custom type needed a SAS UI step first.

  • An edit keeps each attribute's identifier, which matters more than it looks: every term's stored values are filed under it, and re-minting one orphans them all.
  • To rename an attribute, give its attribute_id alongside the new label. Matching by label alone means a new label matches nothing, mints a fresh attribute, and leaves every stored value behind under the old one — readable as nothing. A rename onto a label another attribute already holds is refused rather than producing two of the same name.
  • A single-select with no allowed_values is refused, since nothing could ever be stored in it — the service accepts that definition happily.

📝 A draft is a separate resource

create_glossary_term(publish=false) produced a term that could not be edited and could never be published. /glossary/terms/{id} answers GET and DELETE for a draft, so it looked reachable — but a PUT there is a flat 404.

The draft's own links say where it actually lives: PUT …/draft to edit, PUT …/draft/state?action=publish to promote. Neither is documented anywhere. Updates now route to the draft when the term is one, and update_glossary_term(publish=true) publishes it — on its own, or alongside a final edit.

🔍 Attributes in listings, and filtering on them

include_attributes returns each term's attribute values from list_glossary_terms and search_glossary_terms; attribute_filter keeps only the terms that match — {"Used in Risk": true}, {"Regions": "EMEA"}.

On the listing, attributes are free: the list representation already carries the raw UUID-keyed map, so decoding costs one call per distinct term type on the page — normally one — and none per term.

The glossary cannot filter on attribute values, so this filters here, and says how far it got: scanned, scan_complete, and a next_start when the scan was capped. A short list is otherwise indistinguishable from a complete answer. Narrowing with term_type narrows the scan too — on a live 1,720-term glossary, one term type answered in 1.3 s against 8.3 s unscoped.

💬 Errors that say what went wrong

Every Viya failure used to lead with httpx's status phrase, the full URL and a link to the MDN page for the status code, ahead of the one line that says what was actually wrong. Errors now read:

HTTP 400 from POST /glossary/terms — Viya reported: The value "true" for the field "Approved" is invalid

This one is not glossary-specific — it affects every tier.

🔢 Tool count

91 tools across tiers 0–9 — up from 87. 50 read-only (unchanged: all four new tools write), 21 destructive. Tier 9 goes from 12 tools to 16.

No tool was removed or renamed. import_glossary_terms' created keeps its old meaning — rows the job accepted — with new and already_existed added beside it.

🐳 Container image

docker pull ghcr.io/sassoftware/sas-mcp-server:1.14.0

Helm chart 0.2.4 (appVersion 1.14.0); both sample manifests and the pull/mirror instructions point at 1.14.0.

⚠️ Upgrading

Drop-in. Container and Kubernetes deployments pick it up with the new image tag; a source checkout wants the usual uv sync.

One thing to check if you already authored terms with v1.13.0: boolean and multi-select attributes written through the old tools did not store what you sent — a boolean fell back to the term type's default, and a multi-select was rejected outright. Re-read any term carrying one before trusting it.

📄 Details

v1.13.0 — Business Glossary

Choose a tag to compare

@Criptic Criptic released this 04 Sep 11:21
0cf5d81

Highlights

📖 Tier 9 — Business Glossary: twelve new tools (#51)

Tier 1 can tell you a column is named CD_NAC_RSK, that it is a varchar(3), and which table it lives in. It cannot tell you what it means, who decided that, or whether anyone has decided at all. That meaning lives in the SAS Business Glossary, and this release is the server learning to read and write it.

The gap matters most for exactly the questions people bring to an MCP server. "Which tables carry counterparty exposure?" is a glossary question wearing a data-discovery costume — the answer depends on which columns a governance team attached a term to, not on column names, which are frequently unhelpful (CD_NAC_RSK) or actively misleading (two AMOUNT columns meaning different things).

Reading Linking Authoring
search_glossary_terms — ranked free text over names and definitions list_term_assets — where is this term used? create_glossary_term
list_glossary_terms — structural: by type, parent, name list_table_terms — what does every column of this table mean? update_glossary_term
get_glossary_term delete_glossary_term
list_glossary_term_types / get_glossary_term_type assign_glossary_term / unassign_glossary_term

Enable the tier alone with MCP_TIERS=9. Seven of the twelve survive MCP_READ_ONLY.

🧩 Three sharp edges the tier absorbs

The Glossary REST API can answer all of the above, but three of its properties make a direct wrapping quietly wrong rather than loudly broken.

A term has two identities, and the bridge between them is invisible by default. Every term exists twice — as a Glossary object and as an Information Catalog entity, under different ids. The glossary id is what you read and write; the catalog id is what asset relationships point at and what catalog search returns. Get them the wrong way round and the call 404s.

They are joined by the catalog entity's resourceId, and that field is absent from the default representation. Read the entity as anything other than application/vnd.sas.metadata.instance.entity+json and resourceId is simply not in the body — no error, no warning, just a missing key. This is the failure mode worth knowing about, because it does not look like a failure: code reading resourceId from the default representation gets None for every term and therefore reports "this table has no glossary terms" for a table that is fully governed. Every tool here returns both ids explicitly, as term_id and catalog_entity_id.

Custom attributes are keyed by UUID, with the labels held elsewhere. A term's attributes come back as {"8f3c…e21": "Group"}; the human label (Scope) lives on the term type, a separate resource. These tools resolve UUID→label on read and label→UUID on write, so a caller works in {"Scope": "Group"} throughout. They also drop the empty values the glossary stores for every declared attribute, validate single-select values and required attributes before calling Viya — naming the valid labels, which is a mistake a model can correct where an HTTP 400 is not — and coerce booleans to the "true"/"false" strings the glossary stores, since passing a real bool is accepted and then displays as True forever.

Term↔asset links are catalog relationships, and their endpoints are representation-dependent too. endpoint1Id/endpoint2Id appear only under application/vnd.sas.metadata.instance.relationship+json. Supplying that as the collection's Accept-Item returns the endpoints straight from a filtered query, instead of one round trip per link. On a live 13-column table, measured end to end:

requests wall clock
per-link traversal 27 4.83 s
batched Accept-Item 4 0.89 s

Identical answers.

✍️ Two authoring defaults worth knowing

  • create_glossary_term publishes by default. The API's ?publish parameter defaults to false, so a plain POST creates a draft visible to nobody but its author — almost never what "create a term" means when a model does it on your behalf. publish=false still gets you the draft.
  • update_glossary_term merges rather than replaces. The API's update is a whole-resource PUT, so the naive wrapping blanks every field you did not mention. This reads the current term first and merges; attributes merge one at a time.

📄 Reading every asset of a heavily used term

list_term_assets takes limit (default 100, ceiling 500 — the catalog's page size) and start, and reports the term's total asset count alongside the page, plus truncated and the exact next_start to pass. A term governing thousands of columns is read in full by paging, rather than being cut off at a fixed ceiling with no way past it.

🔎 Also reachable from Tier 1

Terms assigned this way become searchable through catalog_search using the Column.term:"<term name>" facet on the datasets index, which returns the tables carrying a term without resolving individual columns. Matching is case-insensitive. (The catalog's own index for terms is terms, plural — singular term returns HTTP 400.)

🔢 Tool count

87 tools across tiers 0–9 — up from 75. 50 read-only (was 43), 18 destructive. Nothing existing changed shape: no tool was removed, renamed, or altered in behaviour.

🐳 Container image

docker pull ghcr.io/sassoftware/sas-mcp-server:1.13.0

Helm chart 0.2.3 (appVersion 1.13.0); both sample manifests and the pull/mirror instructions point at 1.13.0.

⚠️ Upgrading

Drop-in. Container and Kubernetes deployments pick it up with the new image tag; a source checkout wants the usual uv sync. Nothing in .env, the chart values, or any client configuration needs to change — though if you set MCP_TIERS explicitly, Tier 9 is not included unless you add it.

📄 Details

v1.12.0 — FastMCP 4.0

Choose a tag to compare

@Criptic Criptic released this 02 Sep 07:48
8e09fbf

Highlights

⬆️ FastMCP 4.0 is now the minimum (#50)

The pin moves from fastmcp>=3.0.0,<4.0.0 to fastmcp>=4.0.0,<5.0.0, bringing mcp 2.x and FastMCP 4's HTTP client, httpx2, alongside the httpx this project uses directly.

Nothing about how this server is written had to change. Tools, prompts, middleware, the tier system, read-only mode and the OAuth proxy all work on 4.0 unaltered — the entire test suite passes without a single change to server logic, and the live integration suite passes against a real SAS Viya deployment.

It is not a drop-in for an existing checkout, though. uv sync — or a container rebuild — is required, and running this code against FastMCP 3.x is no longer supported. The next section is why that combination has to be ruled out rather than merely discouraged.

🏷️ Tool annotations renamed to the MCP SDK v2 snake_case fields

MCP SDK v2, which ships with FastMCP 4, renamed the annotation fields: readOnlyHint → read_only_hint, and likewise destructive_hint, idempotent_hint, open_world_hint, plus input_schema in the tests. This release follows that rename.

Clients are unaffected. The wire format is still the spec's camelCase, because SDK v2 keeps those names as serialization aliases. Verified against a real tools/list response: all 75 tools carry the camelCase keys, and 43 report readOnlyHint: true, exactly as before.

🛡️ Guarding the failure mode that has no symptoms

The rename above is exactly the kind of change that can go wrong without anything appearing to break, and two independent versions of that were closed here.

Mixing this code with FastMCP 3.x. On the older mcp 1.x, ToolAnnotations accepts unknown fields, and its real fields are camelCase. Pass read_only_hint=True there and pydantic files it as an extra: the model reads the value back correctly, tests pass, and the real camelCase field stays None. Proven on a live tools/list against 3.4.6 with this source — zero tools advertise read-only instead of 43, so every read-only tool quietly looks writable to a client deciding whether it needs to ask permission. A hard floor of 4.0 is what makes that unreachable.

A future rename doing the same thing to the landing page. The badges were read with getattr(annotations, "readOnlyHint", None), which cannot distinguish "this tool declares nothing" from "this field no longer exists". The next rename would have blanked every badge on the page with nothing failing anywhere. Four tests now close both halves: two assert the field names exist on ToolAnnotations, and two assert the serialized payload uses the spec's camelCase keys and that every read-only and destructive tool still advertises itself as such. Each was confirmed to actually fail when the old name is restored.

🔐 SSL_VERIFY=false still covers FastMCP's own HTTPS calls

FastMCP 4 dropped httpx in favour of httpx2. A TLS patch reaching only httpx would have silently stopped covering the JWKS fetch and the OAuth token exchange — surfacing inside the auth layer as a 401 or a connect error, a long way from its cause.

ssl_patch already patched both, so no change was needed. This release confirms it against 4.0 rather than a pre-release: on a live run with SSL_VERIFY=false, all four client classes carry the patch marker.

🔢 Tool count

Unchanged: 75 tools across tiers 0–8 — 43 read-only, 15 destructive. This release changes the dependency floor and internal field names; no tool was added, removed or renamed, and no tool's behaviour changed.

🐳 Container image

docker pull ghcr.io/sassoftware/sas-mcp-server:1.12.0

Helm chart 0.2.2 (appVersion 1.12.0); both sample manifests and the pull/mirror instructions point at 1.12.0.

⚠️ Upgrading

uv sync   # required — this is not a drop-in for an existing checkout

Container and Kubernetes deployments pick it up with the new image tag. Nothing in .env, the chart values, or any client configuration needs to change.

📄 Details