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
45 changes: 45 additions & 0 deletions .github/workflows/publish-mcp-registry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Publish MCP Registry metadata

on:
workflow_dispatch:
inputs:
ref:
description: Commit, branch, or mcp-v tag containing the reviewed server.json
required: true
default: main
type: string
registry_tag:
description: Registry release identifier, for example mcp-v0.4.345
required: true
type: string

permissions:
contents: read
id-token: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0

- name: Verify MCP Registry metadata
run: node scripts/check-release-version.mjs --channel mcp --tag "${{ inputs.registry_tag }}"

- name: Install MCP Registry publisher
shell: bash
run: |
set -euo pipefail
curl -fsSL \
https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz \
| tar xz mcp-publisher
./mcp-publisher --help

- name: Authenticate with GitHub OIDC
run: ./mcp-publisher login github-oidc

- name: Publish server metadata
run: ./mcp-publisher publish
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
ref:
description: Existing release tag to recover, for example v0.4.179
description: Existing CLI release tag to recover, for example cli-v0.4.181
required: true
type: string

Expand All @@ -27,7 +27,7 @@ jobs:
registry-url: https://registry.npmjs.org
cache: npm
- run: npm ci
- run: node scripts/check-release-version.mjs --tag "${{ inputs.ref }}"
- run: node scripts/check-release-version.mjs --channel cli --tag "${{ inputs.ref }}"
- run: npm run lint
- run: npm test
- run: npm run pack:dry-run
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Create Release

on:
push:
# CLI releases only: a v<CLI semver> tag publishes @xmemo/client to npm.
# CLI releases only: a cli-v<CLI semver> tag publishes @xmemo/client to npm.
tags:
- 'v*.*.*'
- 'cli-v*.*.*'

permissions:
contents: write
Expand All @@ -30,7 +30,7 @@ jobs:
run: npm ci

- name: Verify tag and release metadata
run: node scripts/check-release-version.mjs --tag "${{ github.ref_name }}"
run: node scripts/check-release-version.mjs --channel cli --tag "${{ github.ref_name }}"

- name: Run package gate
run: |
Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,19 +488,23 @@ Normal releases are produced by GitHub Actions from the exact tagged commit,
not from a mutable branch checkout or a developer workstation:

```text
develop → version sync → test → tag → GitHub Actions → npm publish --provenance
develop → CLI version sync → test → cli-v tag → GitHub Actions → npm publish --provenance
```

Version-bearing files must stay synchronized:
The CLI package and hosted MCP service intentionally have separate version
streams:

- `package.json`
- `package-lock.json`
- `server.json`
- `lhm.plugin.json`
- CLI/npm version: `package.json`, `package-lock.json`, and the npm package
entry in `server.json`.
- Hosted MCP/Registry version: the top-level `server.json.version` and
`lhm.plugin.json`. This version follows the deployed XMemo service.

`node scripts/check-release-version.mjs --tag vX.Y.Z` verifies the tag and every
version-bearing file before publication. The separate npm publish workflow is
manual recovery only, so creating a GitHub Release cannot publish twice.
`node scripts/check-release-version.mjs` verifies both contracts. A
`cli-vX.Y.Z` tag must equal the CLI/npm version and publishes only npm. The
MCP Registry is published separately with the `Publish MCP Registry metadata`
workflow using `mcp-vX.Y.Z`, which must equal the hosted MCP/Registry version.
The separate npm publish workflow is manual recovery only, so creating a
GitHub Release cannot publish twice.

## Documentation and support

Expand Down
4 changes: 2 additions & 2 deletions lhm.plugin.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"identifier": "yonro-memory-os-cli",
"name": "XMemo CLI",
"description": "User-owned memory for AI agents over MCP. Save, recall, update, explain, and govern durable context across clients, projects, and sessions.",
"description": "Shared, governed long-term memory for AI agents — durable context across tools and sessions, exposed via MCP and REST.",
"author": "yonro",
"authorUrl": "https://github.com/yonro",
"homepage": "https://xmemo.dev/product/mcp",
"icon": "https://raw.githubusercontent.com/yonro/memory-os-cli/main/plugins/xmemo/assets/logo.png",
"version": "0.4.180",
"version": "0.4.345",
"category": "productivity",
"connectionType": "hybrid",
"cloudEndpoint": "https://xmemo.dev/mcp",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@xmemo/client",
"version": "0.4.180",
"version": "0.4.181",
"description": "Privacy-first CLI and MCP setup helper for XMemo.",
"mcpName": "io.github.yonro/xmemo",
"type": "module",
Expand Down
70 changes: 52 additions & 18 deletions scripts/check-release-version.mjs
Original file line number Diff line number Diff line change
@@ -1,42 +1,76 @@
import fs from 'node:fs/promises';

const expectedTag = readTagArgument(process.argv.slice(2));
const args = process.argv.slice(2);
const tag = readTagArgument(args);
const channel = readChannelArgument(args);
const packageJson = await readJson('package.json');
const packageLock = await readJson('package-lock.json');
const server = await readJson('server.json');
const marketplace = await readJson('lhm.plugin.json');
const expectedVersion = packageJson.version;
const npmPackage = server.packages?.find((item) => item.identifier === packageJson.name);

const versions = [
['package.json', packageJson.version],
const cliVersion = packageJson.version;
const registryVersion = server.version;
const cliMismatches = [
['package-lock.json', packageLock.version],
['package-lock.json packages[""]', packageLock.packages?.['']?.version],
['server.json', server.version],
['server.json npm package', server.packages?.find((item) => item.identifier === packageJson.name)?.version],
['package-lock.json packages[""].version', packageLock.packages?.['']?.version],
['server.json npm package', npmPackage?.version]
].filter(([, version]) => version !== cliVersion);
const registryMismatches = [
['lhm.plugin.json', marketplace.version]
].filter(([, version]) => version !== registryVersion);

const activeChecks = [
...(channel !== 'mcp' ? [['CLI/npm', cliVersion, cliMismatches]] : []),
...(channel !== 'cli' ? [['MCP Registry', registryVersion, registryMismatches]] : [])
];

const mismatches = versions.filter(([, version]) => version !== expectedVersion);
if (mismatches.length > 0) {
for (const [file, version] of mismatches) {
console.error(`${file}: expected ${expectedVersion}, found ${version ?? 'missing'}`);
if (activeChecks.some(([, , mismatches]) => mismatches.length)) {
for (const [scope, expectedVersion, mismatches] of activeChecks) {
reportMismatches(scope, expectedVersion, mismatches);
}
process.exitCode = 1;
} else if (expectedTag && expectedTag !== `v${expectedVersion}`) {
console.error(`Release tag mismatch: expected v${expectedVersion}, found ${expectedTag}`);
} else if (tag && !matchesReleaseTag(tag, channel, cliVersion, registryVersion)) {
console.error(
`Release tag mismatch: expected cli-v${cliVersion} for npm or mcp-v${registryVersion} for MCP Registry, found ${tag}`
);
process.exitCode = 1;
} else {
console.log(`Release metadata is synchronized at ${expectedVersion}${expectedTag ? ` (${expectedTag})` : ''}.`);
if (channel !== 'mcp') console.log(`CLI/npm metadata is synchronized at ${cliVersion}.`);
if (channel !== 'cli') console.log(`MCP Registry metadata is synchronized at ${registryVersion}.`);
}

function matchesReleaseTag(value, selectedChannel, cli, registry) {
if (selectedChannel === 'cli') return value === `cli-v${cli}`;
if (selectedChannel === 'mcp') return value === `mcp-v${registry}`;
return value === `cli-v${cli}` || value === `mcp-v${registry}`;
}

function reportMismatches(scope, expectedVersion, mismatches) {
for (const [file, version] of mismatches) {
console.error(`${scope} ${file}: expected ${expectedVersion}, found ${version ?? 'missing'}`);
}
}

function readTagArgument(args) {
const index = args.indexOf('--tag');
if (index === -1) {
return null;
}
if (index === -1) return null;

const value = args[index + 1];
if (!value) {
console.error('--tag requires a value such as v0.4.179');
console.error('--tag requires a value such as cli-v0.4.181 or mcp-v0.4.345');
process.exit(2);
}
return value;
}

function readChannelArgument(args) {
const index = args.indexOf('--channel');
if (index === -1) return null;

const value = args[index + 1];
if (value !== 'cli' && value !== 'mcp') {
console.error('--channel requires cli or mcp');
process.exit(2);
}
return value;
Expand Down
18 changes: 15 additions & 3 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.yonro/xmemo",
"title": "XMemo",
"description": "User-owned memory for AI agents, Copilot, Claude, IDEs, CLIs, and chat apps over remote MCP.",
"description": "Shared, governed long-term memory for AI agents — durable context across tools and sessions, exposed via MCP and REST.",
"websiteUrl": "https://xmemo.dev/product/mcp",
"repository": {
"url": "https://github.com/yonro/memory-os-cli",
"source": "github"
},
"version": "0.4.180",
"version": "0.4.345",
"remotes": [
{
"type": "streamable-http",
Expand All @@ -31,6 +31,18 @@
"description": "Optional stable local installation id for attribution; XMemo hashes it server-side.",
"isRequired": false,
"isSecret": false
},
{
"name": "X-Memory-OS-Device-ID",
"description": "Optional stable local device id for attribution; use a non-secret machine or installation identifier.",
"isRequired": false,
"isSecret": false
},
{
"name": "X-Memory-OS-Device-Label",
"description": "Optional human-readable local device label for attribution, for example 'MacBook Pro' or 'PC A'.",
"isRequired": false,
"isSecret": false
}
]
}
Expand All @@ -39,7 +51,7 @@
{
"registryType": "npm",
"identifier": "@xmemo/client",
"version": "0.4.180",
"version": "0.4.181",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
Expand Down
14 changes: 12 additions & 2 deletions test/marketplace.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,20 @@ test('LobeHub manifest uses the dedicated MCP server and mirrors its catalog', a
readJson('server.json')
]);

assert.equal(manifest.version, packageJson.version);
assert.equal(serverJson.version, packageJson.version);
assert.equal(manifest.version, serverJson.version);
assert.equal(serverJson.packages[0].version, packageJson.version);
assert.equal(packageJson.bin['xmemo-mcp'], 'bin/mcp-stdio.js');
assert.equal(manifest.description, serverJson.description);
assert.deepEqual(
serverJson.remotes[0].headers.map((header) => header.name),
[
'Authorization',
'X-Memory-OS-Agent-ID',
'X-Memory-OS-Agent-Instance-ID',
'X-Memory-OS-Device-ID',
'X-Memory-OS-Device-Label'
]
);

const npmDeployment = manifest.deploymentOptions.find(
(option) => option.installationMethod === 'npm'
Expand Down
Loading