Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 6 additions & 4 deletions apps/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ User-provided capture of the Dev answer card in Claude, showing Arabic text, RTL

## Dependencies

SAGA is installed from the public npm registry at one exact release, `1.0.0-rc.1` (the `next` tag): the stack `@saga-a2ui/react-mcp-stack`
plus its exact-version siblings `@saga-a2ui/core`, `@saga-a2ui/components`, `@saga-a2ui/mcp` and `@saga-a2ui/react`. No private registry,
SAGA is installed from the public npm registry: the stack `@saga-a2ui/react-mcp-stack@1.0.1` plus the siblings it pins exactly,
`@saga-a2ui/core@1.0.0`, `@saga-a2ui/components@1.0.0`, `@saga-a2ui/mcp@1.0.2` and `@saga-a2ui/react@1.0.0`. SAGA versions its packages
independently, so the siblings need not share the stack's version; each resolves to exactly the version the stack pins. No private registry,
credential, SDK tarball, SDK workspace link or SDK-package override is involved, and the manifest pins an exact version rather than a tag or range.
The root `pnpm-workspace.yaml` keeps the SDK's documented `dompurify` and `@hono/node-server` overrides.
The card's address names its compatibility line, `ui://views/answer-card.v1.html` (`VIEW_MAJOR` in `src/server/view-artifact.ts`), not its
bytes: every compatible release is served at the same address, and only a change an already-loaded card cannot handle increases it.
`pnpm check:release --target beta` checks the pinned release against the registry and the lockfile; `--target production` additionally refuses a
prerelease, so Production waits for the stable SAGA release. The Beta and Production deploy scripts run the corresponding gate before any build or deployment.
`pnpm check:release --target beta` checks the pinned release against the registry and the lockfile, including that every SAGA package resolves to
the exact version its dependents pin; `--target production` additionally refuses a prerelease. The Beta and Production deploy scripts run the
corresponding gate before any build or deployment.

The application depends on one SAGA package, the stack `@saga-a2ui/react-mcp-stack`, plus React and React DOM, and imports
only its subpaths: `/faithful-card` (`FaithfulCardApp`, `startFaithfulCardApp`, and the definition's one `.result(...)` reader that reads the whole delivered result,
Expand Down
18 changes: 10 additions & 8 deletions apps/mcp/docs/show-me-ansari-mcp.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions apps/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ansari-mcp",
"version": "0.17.1",
"version": "0.17.2",
"private": true,
"type": "module",
"description": "Ansari's MCP application",
Expand Down Expand Up @@ -31,7 +31,7 @@
"@hono/node-server": "2.1.5",
"@modelcontextprotocol/hono": "2.0.2",
"@modelcontextprotocol/server": "2.4.0",
"@saga-a2ui/react-mcp-stack": "1.0.0-rc.1",
"@saga-a2ui/react-mcp-stack": "1.0.1",
"hono": "4.13.16",
"react": "19.3.1",
"react-dom": "19.3.1",
Expand Down
32 changes: 28 additions & 4 deletions apps/mcp/scripts/check-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ function lockedPackages(lock) {
return result;
}

/**
* Every exact `@saga-a2ui/*` version a resolved SAGA package pins, from the lockfile's snapshots: who pins what, at which
* version. SAGA versions its packages independently, so the siblings need not share one version; each pin must name the
* version the lockfile resolves.
*/
function sagaPins(lock) {
const pins = [];
for (const [, key, block] of section(lock, 'snapshots').matchAll(/^ {2}'?([^'\n]+?)'?:\n((?: {4}.*\n?)*)/gm)) {
const owner = /^(@saga-a2ui\/[^@]+)@/.exec(key)?.[1];
if (!owner) continue;
for (const [, name, value] of block.matchAll(/^ {6}'?(@saga-a2ui\/[^':]+)'?: '?([^'(\s]+)/gm)) {
pins.push({owner, name, version: value.startsWith('file:') ? localVersion(value) ?? value : value});
}
}
return pins;
}

/** pnpm-workspace.yaml `overrides:` entries for SAGA packages. */
function sagaOverrides(workspace) {
return [...(workspace ?? '').matchAll(/^ {2}'?(@saga-a2ui\/[^':]+)'?: '?([^'\n]+)'?$/gm)].map(([, name, value]) => ({name, value}));
Expand Down Expand Up @@ -141,7 +158,8 @@ export function validateRelease({appRoot = defaultRoot, target} = {}) {
for (const name of manifestOverrides) problems.push(`package.json overrides ${name}: remove every SAGA override for ${target}.`);
}

// 3. The resolved graph: one version across every SAGA sibling, equal to the direct pin, from the registry.
// 3. The resolved graph: one version of each SAGA package, each pinned sibling at exactly its resolved version, the stack
// at the direct pin, from the registry.
let packages = [];
if (lock === undefined) problems.push('pnpm-lock.yaml is missing: commit the resolved graph.');
else {
Expand All @@ -156,9 +174,15 @@ export function validateRelease({appRoot = defaultRoot, target} = {}) {
for (const entry of locked.filter(item => item.name !== STACK)) problems.push(`pnpm-lock.yaml records a redundant direct dependency ${entry.name}.`);
packages = lockedPackages(lock);
if (!packages.some(entry => entry.name === STACK)) problems.push(`pnpm-lock.yaml resolves no ${STACK}.`);
const versions = [...new Set(packages.map(entry => entry.version ?? `unrecognised:${entry.ref}`))];
if (versions.length > 1) problems.push(`The resolved SAGA siblings are inconsistent: ${packages.map(entry => `${entry.name}@${entry.version ?? entry.ref}`).join(', ')}. They must share one version.`);
if (versions.length === 1 && stack !== undefined && versions[0] !== stack) problems.push(`The resolved SAGA siblings are ${versions[0]}, but the direct pin is ${stack}.`);
const resolved = new Map();
for (const entry of packages) resolved.set(entry.name, [...(resolved.get(entry.name) ?? []), entry.version ?? `unrecognised:${entry.ref}`]);
for (const [name, versions] of resolved) if (new Set(versions).size > 1) problems.push(`The resolved SAGA siblings are inconsistent: ${name} resolves to ${[...new Set(versions)].join(' and ')}. Each SAGA package must resolve to one version.`);
for (const pin of sagaPins(lock)) {
const actual = resolved.get(pin.name);
if (actual !== undefined && !actual.includes(pin.version)) problems.push(`The resolved SAGA siblings are inconsistent: ${pin.owner} pins ${pin.name}@${pin.version}, but the lockfile resolves ${pin.name}@${actual.join(', ')}.`);
}
const resolvedStack = resolved.get(STACK);
if (resolvedStack !== undefined && stack !== undefined && !resolvedStack.includes(stack)) problems.push(`The resolved ${STACK} is ${resolvedStack.join(', ')}, but the direct pin is ${stack}.`);
const notRegistry = packages.filter(entry => entry.source !== 'registry');
if (release && notRegistry.length > 0) problems.push(`${notRegistry.length} resolved SAGA package(s) are not public registry releases with an integrity hash (${[...new Set(notRegistry.map(entry => entry.source))].join(', ')}); ${target} accepts only registry releases: ${notRegistry.map(entry => entry.name).join(', ')}.`);
const localRelease = packages.filter(entry => entry.source === 'file' && entry.version !== undefined && !CANDIDATE.test(entry.version));
Expand Down
2 changes: 1 addition & 1 deletion apps/mcp/src/version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// One application identity, shared by the server and built view.
export const APP_VERSION = '0.17.1';
export const APP_VERSION = '0.17.2';
39 changes: 20 additions & 19 deletions apps/mcp/test/packaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ test('packaged HTML is self-contained and assets contain bytes',()=>{
}
});

// ---- The SAGA packages: one exact public npm release, never a tag, range, tarball or local file.
// The expected values below were read from the public npm registry (`npm view @saga-a2ui/<name>@1.0.0-rc.1 dist.integrity`),
// not from the files they are compared with.
const SDK_VERSION='1.0.0-rc.1';
const SDK_INTEGRITY:Record<string,string>={
core:'sha512-0a0MAMSlPTslHhG4Id62UEsmLDZw55e5H/rl9zOMTJseeWRfQUWLieuCt5c11nYHy0FqSNXij8BfhyAq60im9Q==',
components:'sha512-djlrxMR8NXz9sN5IjIC1LIlKEBt4VgqhoAc7b4ZepOpmAywZ6YuyrmHJUZSP/FJviigA0IP7Z2iO0OWIwz3WNA==',
mcp:'sha512-7h6vCTfLvLoHDS6QPD+/waewynSZwf6uX+X+WsvvSLaVJzf9t0emqyT0jxw4qd2EeWbT5nOrWyuxEgvC77p7/A==',
react:'sha512-ZFTuDE1IgEzvMewgqzD+niFE9URfLUwWT77tzhB3EyVcDfHTcT8bIdX5FWqEOsftBpwIk3j+mk7n9gcD1aciSg==',
'react-mcp-stack':'sha512-YSeb7xFRgy5oWYqkGlkXilfTVrGFobkG00FmRRwxurjbmvbCvNeJHNRwetnVXwB9WxGoLGNWnm2JYurjIvM0lg==',
// ---- The SAGA packages: exact public npm releases, never a tag, range, tarball or local file.
// SAGA versions its packages independently: the stack pins each sibling to one exact version, which need not be its own.
// The versions and integrity below were read from the public npm registry (`npm view @saga-a2ui/react-mcp-stack@1.0.1
// dependencies`, then `npm view @saga-a2ui/<name>@<version> dist.integrity`), not from the files they are compared with.
const SDK_VERSION='1.0.1';
const SDK_PACKAGES:Record<string,{version:string;integrity:string}>={
core:{version:'1.0.0',integrity:'sha512-vrL4nuxLzFWs98vJKp1P5v3eVFxPAgyu7b17TkHaYqIIAwWDjAN7ZRdyABk5a8MKG7NIWyPFiqLEK04mfn62rA=='},
components:{version:'1.0.0',integrity:'sha512-SLNlcsw6IrRSd087fTp2uGgBHoqdpUnE+o3G3OZvmRqdRCPZhfjO45mofSGnH2VsrpUQjHTtWSUv0X1NPPHq5g=='},
mcp:{version:'1.0.2',integrity:'sha512-gbskrVWOXLnrkS6tSl5FUQMSvx/lankpzQGJFMg/9nHMBup+BoV5kkdjdKyzrHMlrzD5WmpEXIZtAJ403r+hPQ=='},
react:{version:'1.0.0',integrity:'sha512-Pay/Mz2yNcnsoDLkJo8twFEYUoIx3te6n/w/FOo2jfuSrVh7A0HSS7UVEFuo/oT6pkouwJoEZPO5Sqf6NUojOw=='},
'react-mcp-stack':{version:SDK_VERSION,integrity:'sha512-5uu8h9NAZGs9sfvjmFKfxQzWMTpm/0DjKrb5x1TtlyDRkiRCUSuD2qmxQVFI7f5/TIMymc4XYxGAROwacx/XNg=='},
};
const SIBLINGS=Object.keys(SDK_INTEGRITY);
const pinned:string=JSON.parse(readFileSync(join(root,'package.json'),'utf8')).dependencies['@saga-a2ui/react-mcp-stack'];
const SIBLINGS=Object.keys(SDK_PACKAGES);
const ref=(sibling:string)=>`@saga-a2ui/${sibling}@${SDK_PACKAGES[sibling].version}`;
const node=(...args:string[])=>execFileSync(process.execPath,args,{cwd:root,encoding:'utf8'});

test('the manifest pins one exact public version for every SAGA package it imports',()=>{
Expand Down Expand Up @@ -65,12 +66,12 @@ test('every lockfile resolves the five SAGA packages to the public registry with
assert.ok(lockfiles.length>0,'at least one lockfile installs this application');
for(const {label,text} of lockfiles) {
for(const sibling of SIBLINGS) {
const key=`'@saga-a2ui/${sibling}@${SDK_VERSION}':\n resolution: {integrity: ${SDK_INTEGRITY[sibling]}}`;
const key=`'${ref(sibling)}':\n resolution: {integrity: ${SDK_PACKAGES[sibling].integrity}}`;
assert.ok(text.includes(key),`${label} lockfile locks ${sibling} to the registry release with its integrity`);
}
// Nothing else from @saga-a2ui, no second SAGA version, and no local source of any kind for it.
const resolved=[...new Set([...text.matchAll(/^ {2}'(@saga-a2ui\/[a-z-]+)@([^'(]+)':$/gm)].map(match=>`${match[1]}@${match[2]}`))].sort();
assert.deepEqual(resolved,SIBLINGS.map(sibling=>`@saga-a2ui/${sibling}@${SDK_VERSION}`).sort(),`${label} lockfile: exactly the five packages at one version`);
assert.deepEqual(resolved,SIBLINGS.map(ref).sort(),`${label} lockfile: exactly the five packages, each at the version the stack pins`);
assert.doesNotMatch(text,/@saga-a2ui\/[a-z-]+@file:|tarball: file:|@saga-a2ui\/lit/,`${label} lockfile: no local file or Lit package`);
assert.doesNotMatch(text,/(?:\/tmp\/|\/opt\/|\/Users\/|\/home\/|[A-Za-z]:\\)/,`${label} lockfile records no machine path`);
}
Expand All @@ -88,15 +89,15 @@ test('no workspace file overrides a SAGA package',()=>{
assert.equal(existsSync(join(root,'candidates')),false,'no local SAGA inputs are committed');
});

test('the installed tree has strict same-version siblings and one framework runtime',()=>{
test('the installed siblings are the versions the stack pins, with one framework runtime',()=>{
const modules=join(root,'node_modules');
const scope=dirname(realpathSync(join(modules,'@saga-a2ui/react-mcp-stack')));
for(const sibling of SIBLINGS) {
const installed=JSON.parse(readFileSync(join(scope,sibling,'package.json'),'utf8'));
assert.equal(installed.name,`@saga-a2ui/${sibling}`);
assert.equal(installed.version,pinned,`${sibling} is the pinned SAGA version`);
// Every SDK sibling it depends on or needs as a peer is the same version.
for(const field of ['dependencies','peerDependencies']) for(const [dependency,range] of Object.entries<string>(installed[field]??{})) if(dependency.startsWith('@saga-a2ui/')) assert.equal(range,pinned,`${sibling} ${field} ${dependency}`);
assert.equal(installed.version,SDK_PACKAGES[sibling].version,`${sibling} is the version the stack pins`);
// Every SDK sibling it depends on or needs as a peer is pinned to exactly the installed version of that sibling.
for(const field of ['dependencies','peerDependencies']) for(const [dependency,range] of Object.entries<string>(installed[field]??{})) if(dependency.startsWith('@saga-a2ui/')) assert.equal(range,SDK_PACKAGES[dependency.slice('@saga-a2ui/'.length)]?.version,`${sibling} ${field} ${dependency}`);
}
// Only those five are installed.
assert.deepEqual(readdirSync(scope).sort(),SIBLINGS.slice().sort());
Expand Down Expand Up @@ -145,5 +146,5 @@ test('distributed HTML retains complete installed SAGA and font licence notices'
assert.ok(notices.includes(readFileSync(join(dirname(stack), 'components/THIRD_PARTY_NOTICES.md'), 'utf8').trim()));
assert.ok(notices.includes(readFileSync(join(root, 'assets/fonts/OFL.txt'), 'utf8').trim()));
assert.ok(notices.includes(readFileSync(join(root, 'assets/NOTICE.md'), 'utf8').trim()));
for (const name of SIBLINGS) assert.ok(notices.includes(`@saga-a2ui/${name}@${SDK_VERSION}`));
for (const name of SIBLINGS) assert.ok(notices.includes(ref(name)), ref(name));
});
8 changes: 7 additions & 1 deletion apps/mcp/test/release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,13 @@ describe('release input validation (fixture inputs, no registry)', () => {
const skewed = {...STABLE, lockTransform: (lock: string) => lock.replace("'@saga-a2ui/core@0.7.0'", "'@saga-a2ui/core@0.6.9'").replace("'@saga-a2ui/core': 0.7.0", "'@saga-a2ui/core': 0.6.9")};
for (const target of ['beta', 'production']) assert.match(validate(skewed, target).join('\n'), /siblings are inconsistent/);
const wrongPin = {...STABLE, lockTransform: (lock: string) => lock.replaceAll('0.7.0', '0.7.1')};
assert.match(validate({...wrongPin, stackSpec: '0.7.0'}, 'beta').join('\n'), /siblings are 0\.7\.1, but the direct pin is 0\.7\.0|is stale/);
assert.match(validate({...wrongPin, stackSpec: '0.7.0'}, 'beta').join('\n'), /react-mcp-stack is 0\.7\.1, but the direct pin is 0\.7\.0|is stale/);
});

it('accepts independently versioned siblings when every pin names the resolved version', () => {
// SAGA 1.0.x: the stack (here 0.7.0) pins core at its own version; core resolves there and every sibling pins it there.
const independent = {...STABLE, lockTransform: (lock: string) => lock.replaceAll("'@saga-a2ui/core@0.7.0'", "'@saga-a2ui/core@0.6.9'").replaceAll("'@saga-a2ui/core': 0.7.0", "'@saga-a2ui/core': 0.6.9")};
for (const target of ['beta', 'production']) assert.doesNotMatch(validate(independent, target).join('\n'), /siblings are inconsistent|direct pin/);
});

it('rejects a lockfile that was not resolved for the committed pin', () => {
Expand Down
Loading
Loading