Skip to content

Commit 89c2a4a

Browse files
committed
feat: publish agent-comms to the MCP Registry
1 parent a8c6eb7 commit 89c2a4a

6 files changed

Lines changed: 175 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ Add to your MCP server configuration:
101101

102102
The generic MCP bridge works with any MCP client. Incoming messages are included in every tool response.
103103

104+
This server is also published to the MCP Registry as `io.github.exadev/agent-comms`.
105+
104106
### Other harnesses
105107

106108
```bash

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"url": "git+https://github.com/ExaDev/agent-comms.git"
1010
},
1111
"homepage": "https://github.com/ExaDev/agent-comms#readme",
12+
"mcpName": "io.github.exadev/agent-comms",
1213
"bugs": "https://github.com/ExaDev/agent-comms/issues",
1314
"license": "MIT",
1415
"keywords": [
@@ -45,6 +46,8 @@
4546
"scripts": {
4647
"prepare": "husky",
4748
"build": "tsc && printf '#!/usr/bin/env node\n' | cat - dist/cli.js > dist/cli.tmp && mv dist/cli.tmp dist/cli.js",
49+
"generate:server-json": "tsx scripts/sync-release-metadata.ts",
50+
"publish:mcp-registry": "tsx scripts/publish-mcp-registry.ts",
4851
"lint": "eslint .",
4952
"test": "node --test dist/test/**/*.test.js",
5053
"typecheck": "tsc --noEmit"

release.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ const config: GlobalConfig = {
4646
[
4747
"@semantic-release/exec",
4848
{
49-
prepareCmd:
50-
"node -e \"const fs=require('fs');const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));const p=JSON.parse(fs.readFileSync('.claude-plugin/plugin.json','utf8'));p.version=pkg.version;fs.writeFileSync('.claude-plugin/plugin.json',JSON.stringify(p,null,2)+'\\n')\"",
49+
prepareCmd: "tsx scripts/sync-release-metadata.ts",
50+
publishCmd: "tsx scripts/publish-mcp-registry.ts",
5151
},
5252
],
5353
[
@@ -57,6 +57,7 @@ const config: GlobalConfig = {
5757
"package.json",
5858
"pnpm-lock.yaml",
5959
".claude-plugin/plugin.json",
60+
"server.json",
6061
],
6162
message: "chore(release): v${nextRelease.version} [skip ci]",
6263
},

scripts/publish-mcp-registry.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { mkdtempSync, rmSync } from "node:fs";
2+
import path from "node:path";
3+
import { tmpdir } from "node:os";
4+
import { spawnSync } from "node:child_process";
5+
6+
function run(command: string, args: string[]): void {
7+
const result = spawnSync(command, args, { stdio: "inherit" });
8+
if (result.status !== 0) {
9+
throw new Error(`Command failed: ${command} ${args.join(" ")}`);
10+
}
11+
}
12+
13+
const os = process.platform;
14+
const arch = process.arch;
15+
16+
let binaryArch: string;
17+
if (arch === "x64") {
18+
binaryArch = "amd64";
19+
} else if (arch === "arm64") {
20+
binaryArch = "arm64";
21+
} else {
22+
throw new Error(`Unsupported architecture: ${arch}`);
23+
}
24+
25+
const archiveUrl = `https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${os}_${binaryArch}.tar.gz`;
26+
const tempDir = mkdtempSync(path.join(tmpdir(), "agent-comms-mcp-"));
27+
28+
try {
29+
run("bash", [
30+
"-lc",
31+
`curl -fsSL "${archiveUrl}" | tar -xzf - -C "${tempDir}" mcp-publisher`,
32+
]);
33+
run(path.join(tempDir, "mcp-publisher"), ["login", "github-oidc"]);
34+
run(path.join(tempDir, "mcp-publisher"), ["publish"]);
35+
} finally {
36+
rmSync(tempDir, { recursive: true, force: true });
37+
}

scripts/sync-release-metadata.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { readFile, writeFile } from "node:fs/promises";
2+
import path from "node:path";
3+
4+
type JsonObject = Record<string, unknown>;
5+
6+
function isRecord(value: unknown): value is JsonObject {
7+
return typeof value === "object" && value !== null && !Array.isArray(value);
8+
}
9+
10+
function isUnknownArray(value: unknown): value is readonly unknown[] {
11+
return Array.isArray(value);
12+
}
13+
14+
function parseJsonObject(text: string, label: string): JsonObject {
15+
const parsed: unknown = JSON.parse(text);
16+
if (!isRecord(parsed)) {
17+
throw new Error(`${label} must contain a JSON object`);
18+
}
19+
return parsed;
20+
}
21+
22+
function requireString(value: unknown, label: string): string {
23+
if (typeof value !== "string") {
24+
throw new Error(`${label} must be a string`);
25+
}
26+
return value;
27+
}
28+
29+
function stringifyJson(value: JsonObject): string {
30+
return `${JSON.stringify(value, null, 2)}\n`;
31+
}
32+
33+
const root = process.cwd();
34+
const packagePath = path.join(root, "package.json");
35+
const pluginPath = path.join(root, ".claude-plugin/plugin.json");
36+
const serverPath = path.join(root, "server.json");
37+
38+
const packageObject = parseJsonObject(
39+
await readFile(packagePath, "utf8"),
40+
"package.json",
41+
);
42+
const pluginObject = parseJsonObject(
43+
await readFile(pluginPath, "utf8"),
44+
".claude-plugin/plugin.json",
45+
);
46+
const serverObject = parseJsonObject(
47+
await readFile(serverPath, "utf8"),
48+
"server.json",
49+
);
50+
51+
const packageVersion = requireString(
52+
packageObject.version,
53+
"package.json version",
54+
);
55+
const mcpName = requireString(packageObject.mcpName, "package.json mcpName");
56+
const packageName = requireString(packageObject.name, "package.json name");
57+
const serverName = requireString(serverObject.name, "server.json name");
58+
const serverVersion = requireString(
59+
serverObject.version,
60+
"server.json version",
61+
);
62+
const pluginVersion = requireString(
63+
pluginObject.version,
64+
".claude-plugin/plugin.json version",
65+
);
66+
67+
if (serverName !== mcpName) {
68+
throw new Error(
69+
`server.json name (${serverName}) must match package.json mcpName (${mcpName})`,
70+
);
71+
}
72+
73+
const packagesValue = serverObject.packages;
74+
if (!isUnknownArray(packagesValue) || packagesValue.length !== 1) {
75+
throw new Error("server.json must define exactly one package entry");
76+
}
77+
78+
const packageEntryValue = packagesValue[0];
79+
if (!isRecord(packageEntryValue)) {
80+
throw new Error("server.json package entry must be an object");
81+
}
82+
83+
const registryType = requireString(
84+
packageEntryValue.registryType,
85+
"server.json package entry registryType",
86+
);
87+
const identifier = requireString(
88+
packageEntryValue.identifier,
89+
"server.json package entry identifier",
90+
);
91+
92+
if (registryType !== "npm") {
93+
throw new Error('server.json package entry must use registryType="npm"');
94+
}
95+
96+
if (identifier !== packageName) {
97+
throw new Error(
98+
`server.json package identifier (${identifier}) must match package.json name (${packageName})`,
99+
);
100+
}
101+
102+
serverObject.version = packageVersion;
103+
packageEntryValue.version = packageVersion;
104+
pluginObject.version = packageVersion;
105+
106+
if (serverVersion !== packageVersion || pluginVersion !== packageVersion) {
107+
await writeFile(serverPath, stringifyJson(serverObject));
108+
await writeFile(pluginPath, stringifyJson(pluginObject));
109+
}

server.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3+
"name": "io.github.exadev/agent-comms",
4+
"title": "Agent Comms",
5+
"description": "Cross-harness communication mesh for LLM agents — rooms, DMs, presence, and visibility over TCP",
6+
"repository": {
7+
"url": "https://github.com/ExaDev/agent-comms",
8+
"source": "github"
9+
},
10+
"version": "1.8.2",
11+
"packages": [
12+
{
13+
"registryType": "npm",
14+
"identifier": "agent-comms",
15+
"version": "1.8.2",
16+
"transport": {
17+
"type": "stdio"
18+
}
19+
}
20+
]
21+
}

0 commit comments

Comments
 (0)