Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
28815e2
fix(design-tokens): resolve tokens.css from source inside the monorepo
dsklyar Aug 2, 2026
ca962ee
feat(mcp-server-base): negotiate client capabilities from the handshake
dsklyar Aug 2, 2026
3301163
feat(mcp-server-base): attribute stdio sessions to the detected host
dsklyar Aug 2, 2026
00c4927
feat(mcp): serve ui:// views and per-capability tool variants
dsklyar Aug 2, 2026
e067fb8
chore(mcp): build dependencies before the umbrella package's checks
dsklyar Aug 2, 2026
750de3c
feat(mcp-server-base): add a browser-only ./ui subpath with useMcpApp
dsklyar Aug 2, 2026
69c8f4a
feat(mcp-server-base): publish the Tailwind view theme
dsklyar Aug 2, 2026
de730b5
feat(mcp): build MCP App views with React and Vite
dsklyar Aug 2, 2026
1a928d6
fix(mcp): restore the Inspector's missing app sandbox document
dsklyar Aug 2, 2026
e96624f
feat(dev): add mcp-server-examples with the reference view and form f…
dsklyar Aug 2, 2026
4d73673
feat(mcp): add pnpm mcp:inspect with per-request dev view HTML
dsklyar Aug 2, 2026
3dfbe4b
feat(mcp): scaffold a new view with pnpm mcp:new-view
dsklyar Aug 2, 2026
eea68ef
docs(examples): point at the view scaffolder
dsklyar Aug 2, 2026
d0af619
feat(mcp): scaffold the tool a view needs, and install what it imports
dsklyar Aug 3, 2026
dc1825d
fix(mcp): skip an empty view directory instead of failing on it
dsklyar Aug 3, 2026
ce72c44
feat(mcp): render agent-authored dashboards with json-render
dsklyar Aug 4, 2026
bb69ecf
fix(mcp): load markdown assets in tests the way the bundles do
dsklyar Aug 4, 2026
3917172
chore(dev): drop the throwaway styling preview script
dsklyar Aug 4, 2026
80eaf6b
chore(mcp): point sandbox_proxy TODO at ZEL-8153
dsklyar Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/design-tokens-css-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@transcend-io/design-tokens': patch
---

Resolve `@transcend-io/design-tokens/tokens.css` from source inside this monorepo by giving the export an `@transcend-io/source` condition, matching the package's main entry. Published consumers still read `dist/tokens.css`. Terrazzo rewrites `src/tokens.css` in place, whereas tsdown copies it into a `dist/` it has just cleaned, so a package importing the stylesheet could fail to resolve it while design-tokens happened to be rebuilding.
7 changes: 7 additions & 0 deletions .changeset/mcp-caller-attribution-from-host.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@transcend-io/mcp-server-base': patch
---

Fall back to the host detected at `initialize` when setting `x-transcend-mcp-caller` on outbound Transcend requests, so stdio sessions carry usage attribution they previously had no way to send.

An explicitly forwarded header still takes precedence, since a caller proxying on a user's behalf knows its own identity better than we can infer it. Nothing is sent when the host could not be identified, rather than guessing.
11 changes: 11 additions & 0 deletions .changeset/mcp-client-capability-negotiation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@transcend-io/mcp-server-base': minor
---

Negotiate client capabilities from the `initialize` handshake, so a tool can adapt to what the connected host is actually able to render.

Servers now derive the host's capabilities and identity once per connection (`deriveClientCapabilities`, `whatIsTheClient`) and expose them to handlers through an `AsyncLocalStorage` session context, reachable with `getMcpSession()` and `hasCapability()` without threading a server through every call signature. `requestElicitation` asks the host for a form and returns `undefined` when it cannot show one, rather than letting the SDK's own capability check throw and fail the tool call.

Only elicitation and MCP Apps are detected, being the only capabilities a tool can act on differently. Sampling and roots are deliberately excluded: roots is inert for API-backed servers, our target hosts do not implement sampling, and both are deprecated as of the 2026-07-28 spec under SEP-2577.

Nothing changes on the wire yet. Handshakes stay byte-identical, and no tool behaves differently until per-capability variants land.
7 changes: 7 additions & 0 deletions .changeset/mcp-server-base-dev-view-html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@transcend-io/mcp-server-base': minor
---

Add `viewHtml`, which lets a UI resource serve its built document from disk instead of the copy inlined at build time when `TRANSCEND_MCP_DEV_VIEWS` is set.

Production behaviour is unchanged: without the variable the inlined string is returned, so the document is still validated once at construction. With it set, each `resources/read` re-reads the built file, so a view rebuild reaches the host without restarting the server or reconnecting the client.
7 changes: 7 additions & 0 deletions .changeset/mcp-server-base-ui-subpath.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@transcend-io/mcp-server-base': minor
---

Add a browser-only `@transcend-io/mcp-server-base/ui` subpath exporting `useMcpApp`, the React hook a view uses to connect to its host, read the payload the tool sent, and call tools back.

The separate subpath is load-bearing rather than cosmetic: the package root reaches into `node:async_hooks`, GraphQL clients, and OAuth, none of which can run in a sandboxed iframe. Importing only from `/ui` in view code keeps that graph unreachable. React and `@modelcontextprotocol/ext-apps` are optional peer dependencies, so packages that ship no view install nothing new.
9 changes: 9 additions & 0 deletions .changeset/mcp-server-base-view-theme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@transcend-io/mcp-server-base': minor
---

Publish `@transcend-io/mcp-server-base/ui/theme.css`, the Tailwind theme MCP App views are styled with.

Stock Tailwind is deliberately absent — the default theme is never imported, so `bg-red-500` does not exist and every utility resolves to a host value, a Transcend design token, or a literal fallback. Surfaces, typography, radii, and shadows follow the style variables the host sends at handshake time, so a view looks native in light or dark Claude; brand and status colors come from `@transcend-io/design-tokens` so it still reads as ours; spacing stays on Tailwind's scale, which the MCP Apps spec omits on purpose because layouts break when it shifts underneath them.

The theme also replaces Tailwind's Preflight, because a view lives in an iframe the host measures: the body has to stay transparent and nothing may trap content in its own scroller. `tailwindcss` and `@transcend-io/design-tokens` are optional peer dependencies, so packages that ship no view install nothing new.
10 changes: 10 additions & 0 deletions .changeset/mcp-ui-resources-and-tool-variants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@transcend-io/mcp-server-base': minor
'@transcend-io/mcp': minor
---

Serve `ui://` HTML resources and resolve tools to a per-capability variant, so one tool definition can return plain text to a scripted client, a form to a host that supports elicitation, and an interactive view to a host that supports MCP Apps (SEP-1865).

`defineToolWithCapabilities` declares the variants; `buildMcpServer` resolves them per connection and registers `resources/list` and `resources/read` for any bound views. Tools carry a `_meta.ui.resourceUri` binding, emitted in both the canonical nested and deprecated flat forms because hosts shipped against the earlier draft still read the flat key. App-only tools stay callable through `tools/call` while being hidden from `tools/list`, so a view can reach its own helpers without cluttering the model's tool set.

For a server with no views nothing changes on the wire: the `resources` capability is only declared when at least one `ui://` resource exists, so those handshakes stay byte-identical.
10 changes: 9 additions & 1 deletion .oxfmtrc.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": [".changeset/*.md", "**/__generated__/**", "schema.graphql", "pnpm-lock.yaml"],
"ignorePatterns": [
".changeset/*.md",
"**/__generated__/**",
"schema.graphql",
"pnpm-lock.yaml",
// Vendored verbatim from upstream so `curl | diff` can prove it has not
// drifted; reformatting its inline script would defeat that check.
"scripts/lib/inspector-sandbox-proxy.html"
],
"printWidth": 100,
"tabWidth": 2,
"semi": true,
Expand Down
2 changes: 2 additions & 0 deletions dev/mcp-server-examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Vite-built MCP App views, rebuilt by `pnpm prebuild`
src/ui/generated/
64 changes: 64 additions & 0 deletions dev/mcp-server-examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# `@transcend-io/mcp-server-examples`

Reference MCP App views and capability-aware tools, as a real MCP server you can
point a host at. Development only — this package is `private` and is never
published.

## Running it

```bash
pnpm mcp:inspect --examples # the official Inspector, over stdio
```

That builds the package first, then serves both examples below, rebuilding the
`hello` view on save. `pnpm mcp:new-view examples <name>` scaffolds another view
here.

## Why it is not published

An MCP App view ships as one self-contained document — React, the view's CSS, and
the design tokens all inlined — which is roughly 550 KB per view. While the hello
view lived in `@transcend-io/mcp-server-docs` it was 94% of that package's
published bytes, downloaded by everyone who installs the umbrella server or the
CLI, for a demo tool no end user has a reason to call.

Keeping it here makes that structural rather than a rule someone has to remember:
a `private` package cannot leak into a tarball, and `@transcend-io/mcp` has no
dependency on it, so the umbrella server does not serve it. Only `--examples`
does.

## What the hello example demonstrates

`src/tools/hello_app.ts` is the worked example for `defineToolWithCapabilities`.
One registration serves three experiences, chosen by what the host declared in
`initialize`:

| Host capability | What the caller gets |
| --------------- | ----------------------------------------------------------- |
| none | a plain text greeting |
| elicitation | a host-rendered form asking who to greet |
| MCP Apps | the interactive `hello` view, plus an app-only refresh tool |

`src/ui/hello/HelloView.tsx` covers the parts of the MCP Apps contract a static
document cannot: React state, a `tools/call` round trip from inside the iframe,
and re-rendering from the result the host pushes back.

## What the elicitation example demonstrates

`src/tools/elicitation.ts` is form collection on its own, with no view. It has no
MCP App variant on purpose: precedence is app, then elicitation, then baseline, so
a tool offering both resolves to its view on every host worth testing against —
including the Inspector. Form-only is what keeps the form reachable in the normal
`pnpm mcp:inspect` loop rather than only under `--v1`.

It covers two things `example_hello_app`'s single optional string does not. First,
every field shape the spec allows: a length-bounded string, a titled single-select
(via `oneOf`, not the deprecated `enumNames`), a bounded integer, a titled
multi-select, and a boolean with a default. Second, the four ways a request can
end — the user answers, refuses, dismisses the form, or the host answers with the
wrong types — each reported as a distinct `outcome` rather than collapsed into
"no value". Declining and cancelling stay separate because a refusal should not be
retried and an abandoned dialog reasonably can be.

See [`packages/mcp/README.md`](../../packages/mcp/README.md) for the full guide to
building views, and the layout conventions this package follows.
49 changes: 49 additions & 0 deletions dev/mcp-server-examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@transcend-io/mcp-server-examples",
"version": "0.0.0",
"private": true,
"description": "Reference MCP App views and capability-aware tools. Development only, never published.",
"license": "Apache-2.0",
"bin": {
"transcend-mcp-examples": "./dist/cli.mjs"
},
"type": "module",
"sideEffects": false,
"types": "./dist/index.d.mts",
"exports": {
".": {
"@transcend-io/source": "./src/index.ts",
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"scripts": {
"prebuild": "node ../../scripts/build-mcp-views.ts",
"build": "tsdown",
"build:ui": "node ../../scripts/build-mcp-views.ts",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit",
"typecheck:ui": "tsc -p tsconfig.ui.json --noEmit"
},
"dependencies": {
"@transcend-io/mcp-server-base": "workspace:*"
},
"devDependencies": {
"@modelcontextprotocol/ext-apps": "catalog:",
"@modelcontextprotocol/sdk": "catalog:",
"@transcend-io/design-tokens": "workspace:*",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"tailwindcss": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
},
"engines": {
"node": ">=22.12.0"
}
}
31 changes: 31 additions & 0 deletions dev/mcp-server-examples/src/apps/hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
defineUiResource,
viewHtml,
type UiResourceDefinition,
} from '@transcend-io/mcp-server-base';

// Built from src/ui/hello/ by this package's `prebuild` and inlined here as a
// string by tsdown's `.html` text loader. The document is fully self-contained —
// React, the view's CSS, and the design tokens are all inlined — because hosts
// render views in a sandboxed iframe with no same-origin server to fetch
// anything from.
import HELLO_APP_HTML from '../ui/generated/hello.html';

/** URI hosts fetch to render the hello-world view. */
export const HELLO_APP_URI = 'ui://transcend-examples/hello';

/** Hello-world view proving the MCP Apps render path end to end. */
export const HELLO_APP_RESOURCE: UiResourceDefinition = defineUiResource({
uri: HELLO_APP_URI,
name: 'Transcend MCP App hello world',
description:
'Minimal interactive view that confirms a host can fetch, sandbox, and render a ui:// resource.',
// Reads from disk instead when TRANSCEND_MCP_DEV_VIEWS is set, so `pnpm mcp:inspect`
// picks up a view rebuild without restarting the server.
html: viewHtml({
bundled: HELLO_APP_HTML,
moduleUrl: import.meta.url,
view: 'hello',
}),
prefersBorder: false,
});
13 changes: 13 additions & 0 deletions dev/mcp-server-examples/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env node
import { createMCPServer } from '@transcend-io/mcp-server-base';

import packageJson from '../package.json' with { type: 'json' };
import { getExampleTools } from './tools/index.js';

createMCPServer({
name: 'transcend-mcp-examples',
version: packageJson.version,
requireStartupAuth: false,
oauthScopes: [],
getTools: getExampleTools,
});
11 changes: 11 additions & 0 deletions dev/mcp-server-examples/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export { getExampleTools } from './tools/index.js';

export { ExampleHelloAppSchema, type ExampleHelloAppInput } from './tools/hello_app.js';

export {
ExampleElicitationSchema,
type ExampleElicitationInput,
type FormOutcome,
} from './tools/elicitation.js';

export { HELLO_APP_RESOURCE, HELLO_APP_URI } from './apps/hello.js';
Loading