Releases: txn2/mcp-data-platform
Release list
mcp-data-platform-v1.116.0
The platform has four places content lands, and until this release no surface said what distinguishes them. One statement now answers that question everywhere it is asked: in the instructions the agent receives, on the portal surfaces where someone chooses where to put a file, and in the documentation.
This is the fourth and final part of the resources overhaul, after discoverability (#1012), prompt attachments (#1013), and lifecycle (#1014) in v1.115.0 and earlier.
The problem
A report template saved as an asset is an output the agent believes it produced, so the next session regenerates it rather than reusing it. A reference file pasted into a knowledge page loses the formatting that was the point of keeping it. A durable fact filed as personal memory reaches one person instead of the team. None of these are errors the platform could refuse; they are the predictable result of four surfaces that never stated what they were for.
One statement, one source of truth
Resources are human-uploaded inputs an agent uses as-is: report templates, brand files, data dictionaries, sample payloads, and reference documents. Assets are AI-generated outputs. Knowledge pages are curated facts to search and synthesize. Memory is per-user recall. If it existed before the conversation and the agent should use it verbatim, it is a resource.
That wording lives in exactly one Go constant, instructions.ResourcePositioning. Each other language keeps one definition and no more: a TypeScript constant the portal renders, and quoted paragraphs in the concepts and portal-user documentation.
Drift is a build failure, not a review responsibility. TestResourcePositioningIsVerbatim reads the portal constant and both documentation pages from Go and fails when any of them diverges from the constant. A second test asserts that the resources page and the upload dialog import and render the shared constant rather than inlining the same words, so a future edit cannot leave one portal surface stating the old split while the other states the new one.
The practical consequence is that grepping the sentence finds every place the platform states it, and none of them can be changed alone.
What the agent is told
When a deployment has managed resources, platform_info appends a resources section to the agent instructions carrying the statement plus the rule that makes it actionable:
- Before formatting a deliverable,
searchfor an applicable template or reference resource and follow it rather than inventing a layout. - When the user names a company file ("our template", "the checklist", "the brand header"), resolve it with
searchand read it in full withfetchrather than asking them to paste it. - Material attached to a prompt is authoritative: use it as given.
The section obeys the same tool gate as the instruction baseline: a tool is named only when the caller's persona can actually reach it. A persona denied search is pointed at the resources/list protocol method instead, which is persona-filtered but is not a tool and is therefore always available. A persona denied only fetch keeps the resolve half of the named-file rule and loses the read-in-full half. A deployment with no managed resources gets no section at all.
This replaces a note that steered every caller at resources/list regardless of what they could reach, and that said nothing about when to reach for a resource. With resources joining search and fetch in v1.115.0, that note was pointing agents at the client-picker primitive rather than at the discovery front door the platform steers every session through.
The portal states it where the choice is made
Copy only. No new components or flows.
- The resources empty state and the upload dialog both render the statement.
- The upload dialog spells out what each category means as it is selected, because the category is how the agent is told to treat the file:
templatesare layouts a deliverable must be produced in,playbooksare procedures to follow rather than summarize,samplesare examples to pattern-match against, andreferencesare documents to consult. A custom category has no hint. - The assets and knowledge-page empty states carry one-line cross-references pointing human-authored files at resources. The knowledge one is not gated on edit rights, since a reader who cannot create a page is exactly the person who needs to be told where their file goes instead.
Fixed: an empty filter reported as an empty library
The resources page reported "No resources yet" whenever the list came back empty, including when a search or category filter simply matched nothing. That sent someone off to upload a file the library may well already have contained. The two states are now distinct: an empty scope keeps the positioning statement and the upload affordance, while a filter that matched nothing gets neither.
Making an approved template mandatory
Steering changes what an agent knows to look for. Operators who need more than that now have a documented path.
server.agent_instructions composes beneath the platform baseline in platform_info, is settable in YAML, and is editable at runtime from the admin portal's Agent Instructions page, which writes the config key to the database. The new documentation carries a copy-paste reporting standard requiring an approved template when one exists in scope, plus a persona context.agent_instructions_suffix that scopes a stricter rule to a single audience. Both YAML blocks are validated by the existing documentation-config gate, so they parse against the real configuration shape rather than being illustrative.
Documentation
New page: Content Model, covering the four layers with what each holds, who authors it, how an agent reaches it, and which portal surface owns it; the decision rule; what the agent is told; and the operator rule above.
The User Portal resources section now leads with purpose instead of mechanics, and the assets and knowledge sections carry the cross-references. docs/llms.txt and docs/llms-full.txt are updated per the documentation standard.
On the behavioral claim
The acceptance criteria for this work included an end-to-end scenario in which an agent asked to produce a weekly report retrieves the approved template before generating output.
The deterministic half is tested here: searching "weekly report template" returns the approved template with a resolvable reference, and fetch returns the layout in full rather than by name, using a fixture whose section order is deliberately unguessable so adherence is checkable. The other half, whether a model chooses to follow the instruction, is not something a unit test can assert. It is filed as a benchmark candidate on #982 with three arms isolating the guidance as the discriminating variable, retrieval and adherence scored mechanically from the tool-call trace, and a small premise probe before any suite is built.
Upgrade notes
No migrations, no configuration changes, no API or tool-shape changes. This release is copy, documentation, guidance, and tests.
One behavioral change. A deployment with managed resources enabled will see different text in the agent_instructions field returned by platform_info. Deployments without managed resources are unaffected, and the section is tool-gated per persona as described above. Any operator instructions set through server.agent_instructions continue to compose exactly as before, between the platform baseline and the resources section.
Changelog
Features
- f1abf8c: feat(resources): one canonical positioning statement across the agent, the portal, and the docs (#1063) (@cjimti)
Installation
Homebrew (macOS)
brew install txn2/tap/mcp-data-platformClaude Code CLI
claude mcp add mcp-data-platform -- mcp-data-platformDocker
docker pull ghcr.io/txn2/mcp-data-platform:v1.116.0Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.116.0_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.116.0_linux_amd64.tar.gzArchives and checksums also carry GitHub artifact attestations. Verify with:
gh attestation verify mcp-data-platform_1.116.0_linux_amd64.tar.gz \
--repo txn2/mcp-data-platformmcp-data-platform-v1.115.0
Managed resources become first-class: uploaded reference material is now discoverable through search and readable through fetch, and a resource can be revised in place without breaking the citations that point at it. Tool argument validation is closed at the schema boundary. The second benchmark report lands with the perishable-knowledge study behind it.
Managed resources are discoverable (#1012)
Uploaded reference material joins the universal search corpus as the resources source, indexed over its metadata and over a bounded text prefix extracted from the uploaded file. A data dictionary is now found by a column name that appears only inside it, not just by its name and description.
Search hits carry an mcp:resource:<id> reference that fetch reads in full: text inline at or under the 1 MB threshold the MCP read path already uses, binary as metadata plus the canonical mcp:// URI, MIME type, and size. Each hit also carries an mcp.ResourceLink content block, so a client with native resource support can attach the file directly rather than round-tripping its bytes through the model.
The index lives on the resources row itself (migration 000091), so deleting a resource deletes its index entry and no ghost hit can survive it. The background indexer reads the blob through the same reader contract and bucket the resources/read middleware uses, classifies text families through pkg/contenttype, and denormalizes a 32 KiB prefix onto the row. It tracks two independent gap signals, because they diverge: a transient blob-read failure still produces a valid metadata-only embedding, which alone would close the gap with the file's contents never indexed while coverage read 100%. An object over 8 MB is indexed on metadata alone, since the blob API has no range read.
Visibility is exactly what resources/list computes, applied in SQL before ranking: global material to every caller, persona material to its members, user material to its owner, and nothing at all on an empty scope set. Persona visibility keys on membership resolved from roles, never on the persona a request resolved to, which falls back to the configured default persona for a caller whose roles match none.
Two fixes to the existing resource surfaces ship with it. The by-id REST handlers now gate on visible scopes OR current write authority over the scope, closing a case where an admin could upload persona material and then be unable to manage or remove it; the grant is re-derived from current claims, so a revoked role revokes the access with it. And the resources/read path now shares the search path's text-family test, inline threshold, and orphan detection instead of keeping local copies, so both doors classify the same file the same way. An SVG resource is served as readable text rather than as an opaque blob.
Resource lifecycle: revision, versions, and read accounting (#1014)
Replacing a resource's content keeps its id, its canonical mcp:// URI, and its file name, so every mcp:resource:<id> citation and prompt attachment pointing at it keeps resolving. Delete-plus-re-upload mints a new id and breaks all of them, and it was previously the only way to fix a typo in a template. The uploaded file's own name is ignored for the same reason.
Each revision writes a resource_versions row and moves the resource head in one transaction, with the version number assigned under a row lock so concurrent revisions queue rather than collide. Prior versions are downloadable and restorable, and a restore writes the old bytes forward as a new head revision rather than rewinding, so the trail stays append-only. Retention is bounded by resources.managed.max_versions (default 10, floor 2): a revision past the cap deletes the oldest version rows and their S3 objects, never the key the head points at.
Serving a resource's content now writes a resource_read audit event through one recorder shared by all three doors: MCP resources/read, a search fetch of an mcp:resource:<id> reference, and a REST content download, each recording which door it was. Listing is not a read, and a read that could not be served records nothing. Events roll up into per-resource read counts over 30 and 90 days with a per-surface breakdown; a durable last_read_at column carries the sortable answer past the audit retention window. With audit disabled nothing is recorded and reads serve unchanged.
The portal resource detail view gains a version history panel (replace, download, restore) and a usage panel. The admin table gains a Last read column, a Recently read sort, and a flag on material never read since it was uploaded.
New REST routes on the existing resource handler:
POST /api/v1/resources/{id}/content
GET /api/v1/resources/{id}/versions
GET /api/v1/resources/{id}/versions/{version}/content
POST /api/v1/resources/{id}/versions/{version}/restore
Unknown tool arguments are refused at the schema boundary (#1057)
The platform's own tool input schemas are closed to unknown top-level arguments. A misnamed field is refused before the handler runs, with a message naming the offending property, instead of being dropped by the struct unmarshal and producing a confusing result. Passing parameters to api_invoke_endpoint instead of query_params now returns unexpected additional properties ["parameters"], and the upstream connection is never called.
Closed here: the five api_* schemas, memory_manage, trino_export, manage_prompt, and show_prompts. Already closed and left alone: save_asset, manage_asset, manage_feedback, memory_capture, search, fetch, and apply_knowledge. Closed by construction (reflection-derived schemas emit additionalProperties: false): platform_info, list_connections, platform_find_tools, and every tool from mcp-trino, mcp-datahub, and mcp-s3.
Nested maps stay open where the names belong to someone else: query_params, headers, and body on the api_* tools carry the upstream API's namespace. Gateway-proxied tools keep the upstream server's schema verbatim, strict or not, since the platform forwards those calls and does not own their argument names.
Three argument-handling defects surfaced by the audit are fixed with it. memory_manage decoded source and entity_urns that no command reads, so passing either was a silent no-op; both are gone. manage_prompt published the shared textpatch grammar's occurrence but discarded it, leaving an agent unable to disambiguate a selector match; it is now threaded through. And an SDK argument-validation failure normalized to a generic tool_error, which invites a blind retry of the same call; it now carries code: invalid_arguments, category: client_input, and a corrective hint.
Each closed schema carries a test asserting its published properties equal its handler struct's JSON tags in both directions, so the closure cannot rot into a rejected-valid-call bug.
Benchmark report 2 and the perishable-knowledge study (#1054)
docs/reference/benchmark-report-2.md is the second report in the series, drafted from committed run data. Every statistic regenerates offline from bench/results/ via bench/report/pk_tables.py (stdlib only, Wilson 95% intervals), the same recompute contract report 1 honors.
The report is explicit about its standing: the pre-registered H1a was falsified by the power pre-run, the confirmatory matrix never ran, and every finding is exploratory.
The headline is a two-factor structure. Reliance on stored knowledge is governed by derivability on the strong tier and inverted by capability on the weak one, with the client confound eliminated from both directions by replicating on the raw Messages API through the same grading seam.
The stale-answer cell completes the matrix. Sonnet re-derived the emptied account and answered zero, 8 of 8. Haiku reported the stale note's value in 8 of 8 attempts, six without a single catalog call and two after receiving the empty listing, the stored note overriding fresh contradictory evidence in the transcript. Measured against its own perfect no-knowledge control, the stale note took haiku from 8/8 to 0/8: on the weak tier a stale note is strictly worse than no note at all.
Two supporting sweeps are archived with their scope stated. In the recheck-cost sweep, verification held at 8 of 8 across an elevenfold rise in cost and agents paid the full price rather than stopping partway. In the answer-handing sweep, a belief that stated the answer outright made no measurable difference to effort: at both ends of the same cost range the matched comparison is a difference of zero calls. The note was not missed, appearing in all 32 belief transcripts and cited in 22 of 32 final answers, but as corroboration of what the agent had already established for itself.
The study infrastructure lands with the report: a second fixture catalog with a ten-world registry for minimal-pair comparisons, a capture corpus run over the platform's own capture tool, a frozen seed set whose cells are minimal pairs by construction, and cell derivation that computes correct behavior from the world and the belief rather than hand-labeling it. The #1027 specs regenerate byte-identically, so that study's archived runs stay reproducible.
Upgrade notes
Database migrations. Two migrations run on start: 000091_resource_search_index and 000092_resource_lifecycle. 000092 backfills every existing resource as version 1 pointing at its current blob; no blob is copied.
Strict tool arguments are a behavior change. A client that was silently passing an argument the platform never read now receives an error naming the property instead of a result. This is the point of the change, but check any automation that constructs tool calls by hand against the tool's published schema before upgrading. Gateway-proxied tools are unaffected.
Resource content indexing requires the index queue. Content search over upload...
mcp-data-platform-v1.114.0
Thread comments can address a teammate by name, the MCP gateway no longer truncates large upstreams to their first 1,000 tools, and the documentation now describes the deployments the code has supported for some time.
@-mention tagging on feedback threads (#1055, closes #627)
Typing @ in a thread composer opens a type-ahead over the people who can already open the item being discussed. Picking one inserts @marcus.johnson(example.com), and the timeline renders it as a name chip.
The token form is @local(domain) rather than @user@example.com. The parenthesized domain terminates the token, so a mention written immediately before sentence punctuation is not absorbed into it, and it matches the parenthesized shape the platform already uses for compound references such as mcp:connection:(kind,name). The body stores an address, never a display name, so a mention keeps working after someone's name changes; the chip resolves the name at render time and falls back to the address when the directory does not know the person. The grammar is implemented on both sides (pkg/portal/mention/mention.go and ui/src/lib/mentions.ts), each with a round-trip test asserting the composer writes exactly what the server reads back.
Who can be mentioned. Both write paths, the portal REST handlers and the manage_feedback tool, run the body through one audience filter before anything is stored. The audience mirrors the portal's own view checks, so a mentionable person is always someone who can open the item:
| Target | Audience |
|---|---|
| Asset | Owner, recipients of active direct shares, recipients of active shares on a collection holding it |
| Collection | Owner, recipients of active shares |
| Prompt | Everyone for a persona- or global-scoped prompt; owner and share recipients for a personal one |
| Knowledge page, standalone channel | The known-users directory (both are open to any authenticated user) |
A name outside that set, and the author's own address, stays plain text: nothing is recorded, nothing is chipped, nothing is delivered. A comment therefore cannot mail an item's title and excerpt to someone who cannot open the item. The composer says so while the author is still typing.
Eligible mentions are recorded on the event as metadata.mentions and notified in their own category with a per-user preference, so muting general thread activity still leaves a person reachable by name. A Mentions of me inbox tab reads them through a GIN-indexed containment query, re-checked against present-day access so a revoked share stops surfacing an item's threads.
Two notification gaps closed alongside it
Both predate this release and are fixed here because the mention audience path depends on them.
Share recipients never heard about comments. NotifyThreadEvent fanned out to exactly two candidates, the target's owner and the thread author, minus the actor. Sharing an asset you own and then commenting on it notified nobody. Grantees are now candidates.
The MCP reply path had no notifier at all. manage_feedback reply appended the event straight to the store, so a comment left through an agent notified nobody, ever. It now fires the same trigger as the portal path.
Widening the fan-out changed the arithmetic of the per-actor rate limit, so that was reworked rather than left to drop mail silently. Fanning out to a target's share list now costs one token rather than one per recipient: the size of that list is a property of the item, not something the author chose, and charging per recipient meant one comment on a widely-shared asset could exhaust the author's budget and drop their next share email as collateral. Mentions and shares stay charged per recipient, since those addresses the author does choose. A mention only removes someone from the general fan-out when it was actually queued, so a person who muted the mention category still receives the comment notification they would have had without being named.
Email delivery is owned by the HTTP composition root, where a long-lived send worker belongs. Under stdio the feedback tool stores replies and mails nothing.
MCP gateway: tools/list pagination (#1052)
The gateway read a single tools/list page from upstream MCP servers. The SDK server pages at 1,000 tools, so any upstream exposing more than 1,000 tools was silently truncated to its first page, with the remainder never registered and never visible in discovery. Nothing surfaced the loss.
Discovery now iterates pagination cursors to exhaustion (pkg/toolkits/gateway/upstream.go, via session.Tools). The regression test in pkg/toolkits/gateway/pagination_test.go serves 7 tools at a page size of 2, so a single-page read observes 2 and drops 5. The bench MCP client carried the same single-page read and gets the same fix.
Operators proxying a large upstream should expect its full tool inventory to appear after upgrading. Persona allow/deny patterns apply to the newly visible tools exactly as they already did to the first page.
Documentation: DataHub backs cross-enrichment, it is not a prerequisite (#1056, closes #1030)
The positioning statement described DataHub as a required backend. The code does not agree: createSemanticProvider returns a noop provider for the noop and empty-string cases, and the query and storage providers do the same, so omitting semantic: and query: is a supported configuration rather than an error. Prospective adopters with no data warehouse were reading "the only required backend is DataHub" and concluding the platform did not fit.
A new Deployment Shapes page names three shapes and gives each a configuration:
- Semantic stack (DataHub, optionally Trino and S3) for cross-enrichment
- API and knowledge on PostgreSQL alone, with no warehouse and no catalog: the API and MCP gateways, API catalogs, the knowledge layer, memory, the portal, and
search/fetchover all of them - Combined, where both sets of surfaces are present at once
Shape is documented as orthogonal to operating mode, which remains solely a question of whether database.dsn is set. The catalog-free section states what the shape gives up as plainly as what it provides: cross-enrichment, the trino_* / s3_* / datahub_* tools, catalog search results, and apply_knowledge with sink: datahub.
Cross-enrichment remains the platform's core feature and lead differentiator throughout. README, the docs landing page, the home template, the server overview, The Data Stack, CLAUDE.md, docs/llms.txt, and docs/llms-full.txt were updated to match.
apply_knowledge no longer reports catalog writes it cannot perform
Found while verifying the catalog-free shape against a running server. When no DataHub connection resolves, the platform substitutes a noop writer whose write methods return nil having reached no system, and apply_knowledge completed the full apply path against it, returning "Changes applied to DataHub", a change count, and a revertible changeset id while persisting nothing anywhere. The operator was left holding a rollback handle for a change that never happened.
The apply path now refuses before the confirmation round-trip, and RevertChangeset refuses a DataHub changeset for the same reason. The rollback gate sits inside RevertChangeset, which the tool and the admin REST endpoint already share, so the two surfaces cannot drift. Both refusals name the way forward rather than only the failure: the blocked change types, knowledge.apply.datahub_connection, and sink: knowledge_page as the catalog-free destination. The admin rollback endpoint answers 409 Conflict for this condition instead of 500.
Two behaviors are deliberately preserved: add_prompt creates a platform prompt rather than a catalog write and still applies, and the knowledge-page sink is untouched in both directions.
Dependency security (#1053)
All 7 open Dependabot alerts patched. Dependency-only; no application source lines changed.
| Severity | Package | Ecosystem | From | To | Advisory |
|---|---|---|---|---|---|
| Critical | github.com/getkin/kin-openapi | go.mod | 0.142.0 | 0.144.0 | GHSA-r277-6w6q-xmqw |
| Critical | github.com/getkin/kin-openapi | bench/go.mod | 0.142.0 | 0.144.0 | GHSA-r277-6w6q-xmqw |
| High | postcss | ui | 8.5.15 | 8.5.23 | GHSA-r28c-9q8g-f849 |
| High | js-yaml | ui | 4.2.0 | 4.3.0 | GHSA-52cp-r559-cp3m |
| High | brace-expansion (1.x) | ui | 1.1.13 | 1.1.16 | GHSA-3jxr-9vmj-r5cp |
| High | brace-expansion (2.x) | ui | 2.0.3 | 2.1.2 | GHSA-3jxr-9vmj-r5cp |
| High | brace-expansion (5.x) | ui | 5.0.6 | 5.0.7 | GHSA-3jxr-9vmj-r5cp |
The kin-openapi advisory is a fail-open authentication bypass in ValidationHandler.Load() via the NoopAuthenticationFunc default; the package is a direct dependency of pkg/toolkits/apigateway, pkg/platform, and the bench module. brace-expansion appears at three distinct majors in the UI tree and the advisory patches each major separately, so the override is split by major rather than force-upgrading any consumer across one. npm audit reports 0 vulnerabilities.
API-connection exposure study harness (#1052, relates to #1027)
The #1027 study harness lands in the bench module with its complete k=1 validation matrix archived under bench/results/api-study-pilot/. The study itself is not published: the full matrix (4 arms x 3 tiers x 50 tasks, 600 episodes, zero harness failures) saturates at 94 to 100 percent accuracy in every cell, so that design cannot discriminate exposure architectures on task success. The design doc opens with the measured outcome and the reasons.
What the pilot does establish, measured: search-then-invoke reaches ceiling accuracy at every tier with a fixed ~3.2k-token tool surface, while per-endpoint tool definitions scale linearly to 365,902 tokens at 2,503 operations; hybrid ran...
mcp-data-platform-v1.113.4
Patch release focused on the built-in Prompt Library MCP App (opened by the show_prompts tool). It corrects how the app declares its size and renders its brand bar so it displays at full height with its logo on claude.ai, matching the Platform Info app.
Highlights
- The Prompt Library app now opens at its full 640px height on claude.ai instead of collapsing to a short, mostly-empty panel.
- The app's brand bar (logo and name) now renders reliably at the top of the panel.
- No server, configuration, database, or API changes. The only change is the embedded app HTML, so upgrading requires no action beyond deploying the new binary or image.
Bug Fixes
Prompt Library app rendered collapsed on claude.ai
claude.ai sizes an MCP App iframe by reading the document's height directly at first paint and does not act on the ui/notifications/size-changed message the app sends afterward (upstream host behavior, anthropics/claude-ai-mcp #69). At that first read, only inline attributes on the <html> element are honored, not the stylesheet. The previous build declared the shell height in CSS only and set it from JavaScript after the prompt data had loaded, so the host read the pre-render height and locked the frame short.
The shell height is now declared as an inline style="height: 640px" on the <html> element, present at first paint and independent of when the prompt data arrives. The size is announced at initialization rather than after the first content paint. The stylesheet rule and the size-changed notification are retained for spec-compliant hosts such as Goose, VS Code, and Postman. This mirrors the approach already used by the Platform Info app.
Brand bar could be absent
The brand bar previously shipped hidden and was revealed by JavaScript. It is now present in the markup unconditionally, matching the always-present logo container in the Platform Info app. The logo, brand name, and brand URL continue to come from the same server-injected brand configuration both built-in apps share.
Scope and compatibility
- Affects only the embedded
prompt-browserapp (apps/prompt-browser/index.html); one file changed. - No breaking changes, no new configuration, no migrations.
- Deployments that do not use the Prompt Library app are unaffected.
Full changelog: v1.113.3...v1.113.4
Installation
Homebrew (macOS)
brew install txn2/tap/mcp-data-platformClaude Code CLI
claude mcp add mcp-data-platform -- mcp-data-platformDocker
docker pull ghcr.io/txn2/mcp-data-platform:v1.113.4Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.113.4_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.113.4_linux_amd64.tar.gzArchives and checksums also carry GitHub artifact attestations. Verify with:
gh attestation verify mcp-data-platform_1.113.4_linux_amd64.tar.gz \
--repo txn2/mcp-data-platformmcp-data-platform-v1.113.3
Prompt-browser iframe height
Follow-up to the height work in v1.113.1, which did not resolve the cramped render in Claude.
Claude sizes an MCP App iframe by reading the DOM height directly, once, at an early snapshot, and ignores ui/notifications/size-changed (anthropics/claude-ai-mcp #69). v1.113.1 declared the height at init (an inline <html> height plus a size-changed message and a ResizeObserver on load). The prompt browser fetches its content in two round trips (the platform_info handshake, then the manage_prompt list), so at init it is still a loading spinner, and the early declaration locked the frame at the spinner height. The platform-info app is unaffected because it fetches its content in a single round trip.
This release stops signaling the size at init. The app sets document.documentElement.style.height and installs its ResizeObserver only after the prompt list has rendered (on the list-loaded and error paths), which is the handling #69 documents. size-changed is still sent for spec-compliant hosts. availableDisplayModes is set to ['inline','pip'] to match the platform-info app.
Verified in a local browser render that the height is set only after content loads, with no console errors. The render inside Claude's own iframe host is the real confirmation and is checked at deploy time.
Full changelog
Bug Fixes
Installation
Homebrew (macOS)
brew install txn2/tap/mcp-data-platformClaude Code CLI
claude mcp add mcp-data-platform -- mcp-data-platformDocker
docker pull ghcr.io/txn2/mcp-data-platform:v1.113.3Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.113.3_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.113.3_linux_amd64.tar.gzArchives and checksums also carry GitHub artifact attestations. Verify with:
gh attestation verify mcp-data-platform_1.113.3_linux_amd64.tar.gz \
--repo txn2/mcp-data-platformmcp-data-platform-v1.113.1
Prompt-browser MCP App fixes
This patch fixes three issues with the interactive prompt-library browser (the show_prompts MCP App).
The agent opens the library when you ask to see your prompts
Asking to list, view, see, show, or browse your prompts routed to manage_prompt and returned a text table; the visual browser opened only after being told to use show_prompts by name. The browser is intentionally bound to a separate presentation-only tool (so the UI does not pop up on every agent prompt operation), but the tool descriptions steered the agent to the data tool for any request containing "list". The show_prompts description now owns the human-facing view phrasings ("list my prompts", "show me my prompts", "what prompts do I have", "open my prompt library", "browse my prompts"), and manage_prompt now points those requests at show_prompts while scoping its own list command to the agent's internal data needs. Asking to see your prompts opens the browser directly.
The app renders at a usable height in Claude
The prompt browser rendered in a cramped ~50-100px strip, too short to browse. Claude sizes an MCP App iframe by reading the rendered DOM height directly and does not honor the ui/notifications/size-changed notification the app sent (anthropics/claude-ai-mcp #69); it also snapshots that height early, before the prompt list loads. The apps previously declared their height only via a stylesheet rule and that ignored message, so Claude read the short early snapshot. The height is now declared as an inline style on the <html> element, present in the DOM at first paint, so the rendered document is a fixed-height box by every measurement the host could use, before any data loads. The size-changed message is retained as a fallback for spec-compliant hosts. The prompt browser now renders at 640px (room to browse); the platform-info app keeps its 500px.
The library carries the platform brand
The browser rendered with no logo or brand name, unlike the platform-info app. The server auto-injects the portal logo into the platform-info app config but was skipping the prompt browser. The prompt browser now inherits the same resolved brand (logo and name) and renders a brand bar above the library, mirroring the platform-info app's logo treatment and default-logo fallback.
Full changelog
Bug Fixes
- fix(mcpapps): route prompt viewing to show_prompts, real iframe height, brand the prompt library (#1049) (@cjimti)
Installation
Homebrew (macOS)
brew install txn2/tap/mcp-data-platformClaude Code CLI
claude mcp add mcp-data-platform -- mcp-data-platformDocker
docker pull ghcr.io/txn2/mcp-data-platform:v1.113.1Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.113.1_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.113.1_linux_amd64.tar.gzArchives and checksums also carry GitHub artifact attestations. Verify with:
gh attestation verify mcp-data-platform_1.113.1_linux_amd64.tar.gz \
--repo txn2/mcp-data-platformmcp-data-platform-v1.113.0
Highlights
Structural editing for HTML, JSX, and SVG assets (#1039)
manage_asset and manage_prompt can now edit HTML, JSX, and SVG content in place by CSS selector or HTML heading, not just markdown headings. The content-editing grammar is now syntax-aware, so the same patch / locate / get_content / outline verbs reach the structural content the portal actually holds. An agent can rewrite one card of a JSX dashboard, restate one section of an HTML report, or retitle one group of an SVG chart by addressing it precisely, without reading or resending the rest of the document.
- Selector-addressed regions. A new
selectorargument names an element by CSS selector on any HTML, JSX, SVG, or XML asset. The region is that element's balanced subtree, from its start tag through its matching end tag, soreplace_sectionandmove_sectioncan never cut a tag in half. Supported forms: type (section,Card),#id,.class(which also matches a JSXclassName),[attr], and[attr=value], joined by descendant (space) or child (>) combinators. Known HTML/SVG tags match case-insensitively; a JSX component name matches case-sensitively. - HTML headings. On an HTML asset,
sectionnames an<h1>–<h6>heading and resolves exactly as a markdown#heading does. - Landmarks.
outlineon an HTML/JSX/SVG asset returns the addressable landmarks alongside the heading tree: every element carrying anidor adata-*marker, with its tag, a copyable selector, line, and byte size. For a headingless dashboard, that list is how an agent finds where to patch without reading the body. - Syntax is derived, not guessed. Region naming comes from the asset content type through
pkg/contenttype. Markdown uses headings; HTML/JSX/SVG/XML use headings or selectors; structureless textual content (JSON, CSV, SQL) is directed to anchored edits.
The grammar stays one grammar: no new tools and no new operations. Anchored edits, atomicity, occurrence disambiguation, base_version staleness, dry_run, and the PATCH_* error contract are unchanged. Markdown assets and prompt content are unaffected. New error codes PATCH_BAD_SELECTOR, PATCH_NO_STRUCTURE, and PATCH_UNRESOLVED_MARKUP follow the existing corrective envelope, and PATCH_AMBIGUOUS / PATCH_SECTION_NOT_FOUND extend to selectors.
Other changes
- API gateway: invoke and export by operation_id (#1046). The apigateway toolkit resolves an endpoint by its OpenAPI
operation_idin addition to method-and-path, through a shared address resolver with path-parameter substitution. - Admin resolves any prompt or asset by name (#1042). An administrator can resolve any user's personal prompt or asset by name, matching the unrestricted-admin access model applied elsewhere in the platform.
- Dependency updates.
google.golang.org/grpc1.81.1 to 1.82.1 (#1036), and an npm group bump across the UI (#1037).
Full changelog
Features
- feat(textpatch): selector-addressed structural editing for HTML/JSX/SVG (#1039) (#1048) (@cjimti)
- feat(apigateway): address invoke/export by operation_id (#1046) (#1047) (@cjimti)
Bug Fixes
- fix(prompt,portal): admin resolves any prompt or asset by name (#1042) (#1045) (@cjimti)
- fix(mcpapps): stable 500px height for prompt-browser and platform-info apps (#1044) (@cjimti)
Others
- build(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#1037) (@dependabot[bot])
- deps: bump google.golang.org/grpc from 1.81.1 to 1.82.1 (#1036) (@dependabot[bot])
Installation
Homebrew (macOS)
brew install txn2/tap/mcp-data-platformClaude Code CLI
claude mcp add mcp-data-platform -- mcp-data-platformDocker
docker pull ghcr.io/txn2/mcp-data-platform:v1.113.0Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.113.0_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.113.0_linux_amd64.tar.gzArchives and checksums also carry GitHub artifact attestations. Verify with:
gh attestation verify mcp-data-platform_1.113.0_linux_amd64.tar.gz \
--repo txn2/mcp-data-platformmcp-data-platform-v1.112.0
MCP Apps work under the session gate, and the prompt browser now appears only when a human asks to see their prompts. This release fixes two defects that made the prompt-browser app unusable in session-gated deployments and made it render on operations where it did not belong.
MCP App calls no longer refused by the session gate (#1040)
An MCP App runs in a sandboxed panel and makes its own tool calls over the same authenticated transport as the agent. With the session-handle requirement on, the platform refused any gated call that did not carry a platform_info-minted session_id, and an app cannot obtain and thread that handle from its sandbox. So the app's calls were refused with SESSION_REQUIRED, and users saw a stack of red errors wherever a prompt app was on screen.
This affected manage_prompt specifically because it was the only tool with an app bound to it. Every other tool threads the handle and worked normally, which is why the rest of the platform was unaffected. Production logs made the shape clear: the agent's calls carried the handle and succeeded, the app's calls carried none and were refused, interleaved on the same authenticated identity in the same second.
The gate no longer refuses an authenticated caller. A call carrying a valid handle is validated and used exactly as before. A call that arrives authenticated but without a handle now adopts the caller's own most-recently-active session, resolved from their authenticated identity, so an app's sandboxed call is scoped to the session the caller already established instead of being rejected. Only a caller with no session at all is refused, so platform_info stays structurally required for a genuinely fresh agent.
The session requirement is not weakened. A compliant agent still threads and validates its handle exactly as before, so its behavior is unchanged. The fallback only affects calls that arrive without a handle, which in practice are an app's. The authenticator, not the handle, remains the security boundary.
The prompt browser renders only when the human asks (#1040)
An MCP App is a UI for the human, but the protocol renders it in response to a tool call, and tool calls are made by the agent. The prompt-browser app was bound to manage_prompt, and the MCP Apps spec renders an app on every call to the tool it is bound to, with no way to condition on the command. So every prompt operation the agent performed for its own work, including resolving, running, and editing prompts, rendered the library browser in front of the human, uninvited.
A new presentation-only tool, show_prompts, now carries the browser. Its only job is to render the prompt library for the human to look at. It performs no data operation an agent would call it for and returns only a short confirmation, so an agent invokes it only when the user asks to see their prompts ("show me my prompts", "open my prompt library"). manage_prompt keeps every data command (resolve, run, create, edit, and the content verbs from the previous release) and carries no app, so the agent editing a prompt renders nothing. The rendered browser populates itself from its own manage_prompt calls, which succeed because of the session-gate fix above.
Net effect: the browser appears on human intent, never on the agent's own bookkeeping, and it stays available in the chat for users who never open the portal.
Upgrading
No migration and no required configuration change.
sessions.handles.require keeps the same default (true) but its meaning is refined: it now means a gated caller must have an established session, not that a handle must be threaded on every single call. Deployments that set require: false for a softer landing can now set it back to true and keep full enforcement, since an app's handle-less calls are scoped by identity rather than refused.
show_prompts is a new tool. Personas that allow all tools (allow: ["*"]) get it automatically. A persona with an explicit tool allowlist that should offer the in-chat prompt browser must add show_prompts to its allow list; without it the agent cannot open the browser for that persona, though all prompt data operations on manage_prompt are unaffected.
Installation
Homebrew (macOS)
brew install txn2/tap/mcp-data-platformClaude Code CLI
claude mcp add mcp-data-platform -- mcp-data-platformDocker
docker pull ghcr.io/txn2/mcp-data-platform:v1.112.0Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.112.0_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.112.0_linux_amd64.tar.gzArchives and checksums also carry GitHub artifact attestations. Verify with:
gh attestation verify mcp-data-platform_1.112.0_linux_amd64.tar.gz \
--repo txn2/mcp-data-platformChangelog
Bug Fixes
- 3a0238f: fix(mcpapps): app-bound tools work under the session gate, and the prompt browser renders only when the human asks (#1040) (#1041) (@cjimti)
Full Changelog: v1.111.0...v1.112.0
mcp-data-platform-v1.111.0
Prompts stop being just text. A prompt now carries the reference material its procedure depends on, and both prompts and assets can be edited in place instead of regenerated whole.
Editing content without resending it (#1033)
Changing one sentence of a saved report used to cost as much as writing the report. The only way to land an edit was to read the whole body back, regenerate it verbatim with the change folded in, and resend all of it: output tokens proportional to the size of the document rather than the size of the change, and a fresh chance to silently drop an unrelated paragraph every time.
manage_asset and manage_prompt now share one content-editing grammar. Changing one sentence in a 41,605-byte report asset costs 272 bytes of arguments and a 521-byte response, with exactly one line of the stored bytes altered.
Six new actions on each tool:
| Verb | Purpose |
|---|---|
outline |
Heading tree with levels, line numbers, and per-section byte size |
get_content |
Read a span: whole body, one section, or a line range |
stats |
Size, line count, current version, content type, body hash, no body |
locate |
Literal or regex matches with count, line numbers, enclosing section, and copyable context |
patch |
Apply an ordered list of anchored edits |
diff |
Compare two versions, or a pending prompt draft against the approved one |
The intended loop for a large document is outline or locate to decide where, then patch that place. The body crosses the wire in neither direction.
A patch is an ordered list of edits: replace (the default, and an empty replacement deletes), insert_before and insert_after, replace_section, move_section, append and prepend. Anchors are literal text or Go RE2 regular expressions with $1-style capture expansion, and section scopes an anchor to one part of the document.
{
"action": "patch",
"asset_id": "ast_...",
"base_version": 7,
"edits": [
{ "find": "revenue grew 12% year over year", "replace": "revenue grew 14% year over year" },
{ "op": "replace_section", "section": "## Methodology", "text": "## Methodology\n\nRestated..." },
{ "op": "replace", "pattern": "Q[1-4] FY24", "replace": "$0 (restated)", "occurrence": "all" }
],
"change_summary": "correct the YoY figure and restate methodology"
}Anchors are text, never line numbers, because a line number is stale the moment an earlier edit lands while an anchor either matches or errors. An anchor must resolve to exactly one span; zero matches and several matches are both refused with the count, and occurrence (first, last, all, or a 1-based index) is the explicit opt-in. Matching is exact first, with a single retry that normalizes CRLF and trailing whitespace and reports itself as normalized. There is no fuzzy or semantic matching: a plausible-but-wrong edit applied silently to someone's report is worse than a rejection an agent can correct.
Every edit resolves against an in-memory copy and the first failure aborts the whole call, naming the failing edit by index, so a partially applied patch is never written. base_version is optional and refused on mismatch with the current version in the error. dry_run returns the full report and diff without writing.
Responses never echo the new body. They carry the new version, size, a per-edit outcome, and a unified diff of just the changed hunks. Unified diff is deliberately an output format and never an input format: as input it makes correctness depend on line numbers and context lines a model must reproduce exactly, while as output the server generates it from two known strings.
Versioning, review, ownership, and audit are unchanged. An asset patch produces an ordinary new version, so revert still works, and that version now records the caller's change_summary or a generated one instead of a fixed constant, which makes version history readable. A prompt patch goes through the same review gate as any other content edit, so patching an approved global or persona prompt produces a pending draft while the approved snapshot keeps being served.
Every verb is text-only. A PDF, image, or other binary asset is refused with PATCH_NOT_TEXT rather than corrupted. Failures use the platform's corrective error envelope: PATCH_NO_MATCH, PATCH_AMBIGUOUS, PATCH_STALE_BASE, PATCH_NOT_TEXT, PATCH_TOO_LARGE, PATCH_SECTION_NOT_FOUND, PATCH_BAD_PATTERN, and PATCH_BAD_EDIT, each with a hint saying how to recover.
Structural verbs currently recognize markdown headings. Anchored edits, locate, get_content, stats, and diff work on any textual content; extending outline and the section operations to HTML, JSX, and SVG is tracked in #1039.
Prompts carry their reference material (#1013)
A prompt is a procedure, and a procedure depends on material: the report template it fills, the checklist it follows, the brand header it embeds, the sample payload it matches. That material had no channel. An author either pasted file contents into the prompt text, which breaks for binary and drifts from the uploaded original, or wrote "use the standard template" and hoped.
A prompt now carries an ordered list of attached managed resources. The link stores a resource id rather than a copy, so editing the uploaded file updates every prompt that attaches it.
A resolved prompt delivers the material after the prompt text, in the forms MCP already has: text at or below 64 KiB inline as an embedded resource, anything binary or larger as a resource link the client reads on demand. Both prompts/get and manage_prompt use serve them, and use additionally lists them in an attachments array with each item's URI, media type, size, and availability, so an agent can state what it received. The framing tells the agent the material is authoritative: fill an attached template rather than inventing a format, follow an attached checklist.
An attachment must be at least as widely visible as the prompt that carries it:
| Resource scope | May be attached to |
|---|---|
| global | any prompt |
persona P |
personal prompts, and persona prompts scoped to exactly P |
| user | the author's own personal prompts only |
Otherwise a shared SOP arrives without its template for most of its audience, and the failure surfaces to a reader who cannot fix it instead of to the one person who can.
The rule is applied at three points: at attach time so the author learns immediately, on every prompt write so a scope change or promotion request that would strand material is refused with a message naming the resource, and at serve time against the caller, where a reader who cannot read an attachment is told only that some materials were not delivered, never their contents and never their names. The write-time check lives on the shared prompt store rather than in each handler, so no write path can widen a prompt past its materials.
Deleting an attached resource does not break the prompt. It still serves, and both the served result and the portal flag the material as missing.
REST, on both the portal and admin surfaces:
GET /api/v1/portal/prompts/{id}/attachments
POST /api/v1/portal/prompts/{id}/attachments
PUT /api/v1/portal/prompts/{id}/attachments
DELETE /api/v1/portal/prompts/{id}/attachments/{resourceID}
GET /api/v1/portal/resources/{id}/prompts
The last one answers what depends on a resource, so the cost of deleting it is visible first. Authors manage attachments from the prompt viewer in the portal, and the resource detail view lists the prompts that attach it.
Fixes
Prompt browser in session-gated deployments (#1032). The built-in prompt browser app called its data tools without a platform session handle, so in a deployment with the session gate on it rendered a SESSION_REQUIRED banner instead of the library. It now calls platform_info before its first data call and threads the returned handle on every subsequent call. The app's tool calls reach the server over the same MCP transport as an agent's and meet the same gate.
Session handling is owned in one place: an in-flight handshake is awaited rather than raced, the handle is merged into arguments once, and a SESSION_REQUIRED, SESSION_EXPIRED, or SETUP_REQUIRED refusal is recovered from once by re-handshaking and replaying. A superseded search can be cancelled without leaving a second waiter behind. A failed handshake is not fatal: the list call runs regardless, because handles may be disabled and a persona may allow manage_prompt while denying platform_info. A deployment with handles disabled returns no handle and nothing is threaded.
Upgrading
Migration 000088 adds the prompt attachment table and applies automatically on startup.
No configuration changes are required and no tool was renamed or removed. The new content actions are additive on manage_asset and manage_prompt, so personas that already allow those tools get them with no policy change. The content argument on update still performs a full rewrite where that is what you want.
Installation
Homebrew (macOS)
brew install txn2/tap/mcp-data-platformClaude Code CLI
claude mcp add mcp-data-platform -- mcp-data-platformDocker
docker pull ghcr.io/txn2/mcp-data-platform:v1.111.0Verification
All release artifacts are signed with Cosign. Verify with:
cosign verify-blob --bundle mcp-data-platform_1.111.0_linux_amd64.tar.gz.sigstore.json \
mcp-data-platform_1.111.0_linux_amd64.tar.gzArchives and checksums also carry GitHub artifact attestations. Verify with:
gh attestation verify mcp-data-platform_1.111.0_linux_amd64.tar.gz \
--repo txn2/mcp-data-platformChangelog
Features
mcp-data-platform-v1.110.0
This release makes stored content render as what it actually is, brings the prompt library into the conversation as an interactive MCP App, and settles the portal toolkit's naming on a single word. Content types are now resolved from the bytes at write time instead of trusted from the writer, every asset and managed resource gets a viewer chosen by family, and the save_artifact / manage_artifact tools become save_asset / manage_asset.
Breaking: save_artifact and manage_artifact no longer exist. See Upgrade notes.
Content-type detection and per-family viewers (#1007, PR #1028)
An api_export that pulled JSON from an upstream returning text/plain used to be stored as text/plain and rendered as a wall of characters. Content types are now settled from the content itself.
Detection. pkg/contenttype is the one place media types are resolved. Binary families come from http.DetectContentType; JSON, NDJSON, XML, YAML, CSV and TSV are layered on top with their own heuristics, because a byte sniffer calls all six text/plain. Detection reads a bounded prefix (512 B for the binary sniff, 8 KB for structured text) and DetectStream replays that prefix ahead of the untouched remainder, so api_export still streams to storage unbuffered. A specific declaration always wins: detection runs only when the declaration is absent, application/octet-stream, or text/plain. Every write path that accepts outside content goes through it (save_asset, manage_asset action=update, api_export, resource upload, and the REST POST /portal/assets and PUT /portal/assets/{id}/content endpoints the portal UI writes through). trino_export sets exact types from its own formatter and is not sniffed. Aliases collapse to one canonical type per family and object keys follow the detected type, so a bucket listing shows .json rather than .bin. When detection replaces a declaration, the original is recorded in the asset's provenance as declared_content_type.
Detection can only ever land on a passive family. It never produces text/html, text/jsx, image/svg+xml or JavaScript, because those render as executing markup and may only be reached when an author declared them deliberately. Content that sniffs as HTML under a text/plain declaration stays text/plain; with no declaration it becomes text/plain. The XML heuristic rejects HTML and SVG root elements for the same reason. Normalize enforces the RFC 2045 type/subtype grammar, so a value that is not a media type never reaches a Content-Type header.
Viewers. A single registry table (ui/src/components/renderers/registry.ts) replaces the substring-match chain and drives all four surfaces: the portal viewer, the public and guest viewers, collection items, and the resources detail view. It also carries what a component cannot decide for itself, namely whether the source editor is offered, whether bytes are embedded or streamed, and the inline size ceiling.
| Family | Viewer |
|---|---|
| JSON | Collapsible virtualized tree, search across keys and values with match count and jump-to-match, JSONPath breadcrumb with copy-path and copy-value, tree/formatted/raw views |
| JSON Lines | One expandable row per record, each opening into the JSON viewer |
| CSV, TSV | Sortable, searchable table |
| Images | Zoom and pan, checkerboard backing for transparency, dimensions and size readout |
| Audio, video | Native players with working seek |
| Embedded viewer with a download fallback | |
| XML, YAML, SQL, Python, JavaScript, plain text | CodeMirror, read-only, with folding, line numbers and a wrap toggle |
| Everything else | A metadata card naming type and size with a download action, instead of raw bytes in a <pre> |
The source editor gains JSON, YAML, SQL and Python modes and a JSON parse-error gutter, sharing its language table with the read-only viewer. The inline size limit is per family: media and PDF stream from the content endpoint and have none, the virtualizing JSON and tabular viewers hold a 32 MB limit, and text families keep the 2 MB cutoff with a download fallback. Assets stored before this release still carry generic types, so the same rules run client-side in ui/src/lib/contentType.ts under the same passive-only restriction; those assets reach the right viewer with no data migration.
Serving. pkg/blobserve is now the single writer for every raw-content endpoint (portal assets, asset versions, thumbnails, public share content, managed resources). All of them carry X-Content-Type-Options: nosniff and a parsed parameter-free Content-Type, use Content-Disposition: attachment for active types and inline for passive families, and support byte ranges so audio and video seek without downloading the whole object. Binary content is served from those endpoints rather than embedded in the page, since the public viewer carries text content as a JSON string and cannot hold arbitrary bytes; its CSP gains media-src and object-src.
One deviation from the issue: PDFs use <object> without a sandbox. Chrome refuses to instantiate its PDF plugin inside any sandboxed frame, verified with a three-way probe. Containment comes from the serving guarantees (parsed application/pdf plus nosniff) and the CSP's object-src 'self'.
New documentation: docs/server/content-viewers.md.
Prompt browser MCP App (#1011, PR #1026)
A built-in prompt-browser MCP App, embedded in the binary and bound to the existing manage_prompt tool through the mcpapps registry via _meta.ui. No new tool is added. In an MCP Apps-capable host a prompt discovery call renders an interactive library browser in the conversation; every other client keeps receiving the same complete structured JSON, so the app is presentation only.
Browse and search. Search-as-you-type debounces into the ranked list query=... mode (hybrid semantic plus lexical, server side); an empty query lists the caller's full visible set. Facets follow the #1010 portal model: All / My Prompts / Library buckets, a collection filter fed by the shared collection list, tag chips, and sorting by usage, recency, name or relevance. Cards show display name, scope and status badges, version, description, run count, approval provenance, collection and tags.
Preview and argument forms. A card expands into a detail view with a provenance panel (invoke-as name, version, status, approver and approval date, owner, runs, last run, collection, tags), a form generated from the prompt's structured argument specs with required markers, description hints and client-side validation, and the full prompt content with {arg} and {{arg}} placeholders highlighted and live-substituted as values are typed, so the user sees exactly what will run.
Run. Submits manage_prompt use with the stable mcp:prompt:<id> handle and the filled arguments. The server substitutes arguments, records the prompt_serve audit event, and returns the rendered content with provenance. The app then attempts ui/message insertion of the rendered prompt (full timeout when the host advertised the message capability, a short attempt otherwise, since a host may support insertion without advertising it) and falls back to showing the rendered content with a copy affordance on rejection or timeout.
Dual-protocol bridge. The postMessage bridge supports both host generations: 2026-01-26 spec hosts (tools/call with JSON-RPC id-correlated responses and hostCapabilities feature detection) and legacy 2025-01-09 hosts such as current claude.ai (ui/call-tool with results delivered as ui/notifications/tool-result and no capabilities object). Legacy results carry no request id, so correlation scans pending waiters oldest-first with per-call shape predicates, at most one list call is in flight, and stale search responses are dropped by sequence. Non-JSON-RPC postMessage traffic is ignored, and all server-controlled strings render through textContent / createTextNode.
Server side. manage_prompt list and search responses now batch-apply audit-derived usage (run_count, last_run_at) to every returned prompt in a single PromptUsage read, and include the shared collections list when the store has the #1010 collection capability. Plain-JSON clients get that enrichment too; it lands in the tool result, not in the app. Built-in app registration in pkg/platform is factored into shared builtinAppDefinition / registerBuiltinApp helpers used by both platform-info and prompt-browser, so operator config merge and assets_path override behave identically for both.
Asset tool rename (#1029, PR #1031)
The portal toolkit's object is an asset everywhere in the system except the two tool names. The tables are portal_assets, portal_asset_versions, portal_asset_thumbnails; the tool result field is asset_id; the REST API, the portal UI and the docs all say asset. Only the tools said artifact, so save_artifact returned an asset_id.
| before | after |
|---|---|
save_artifact |
save_asset |
manage_artifact |
manage_asset |
Hard rename, no aliases. The old names are not registered. MCP tools are discovered at tools/list time rather than pinned by a versioned client contract, so registering the same capability twice adds discovery surface, pollutes the platform_find_tools embedding corpus, and presents one capability as two. scripts/retired-tools.txt now carries both old names, so make doc-check hard-fails if either reappears in docs/, the bench doc tree, or README.md.
Provenance harvesting. MCPProvenanceMiddleware decides when to harvest the accumulated tool-call chain onto a saved asset by matching the called tool name against a configured list, which was a hardcoded []string{"save_artifact"}. A disagreement between that string and the registered name fails silently: provenance capture simply stops. The list now comes from the toolkit'...