validate-mcp-publish-schema.tsā¢8.18 kB
/**
* @fileoverview This script automates the process of preparing and publishing an MCP server
* to the MCP Registry. It performs the following steps in order:
*
* 1. **Sync Metadata**: Reads `package.json` to get the `version` and `mcpName`,
* then updates `server.json` with these values.
* 2. **Validate Schema**: Validates the updated `server.json` against the official
* MCP server schema from the static CDN.
* 3. **Auto-Commit**: Automatically commits the updated `server.json` with a
* conventional commit message, only if there are changes.
* 4. **Authenticate**: Initiates `mcp-publisher login github` and waits for the user
* to complete the browser-based authentication.
* 5. **Publish**: Runs `mcp-publisher publish` to upload the server package to the registry.
* 6. **Verify**: Polls the registry to confirm the server is publicly available.
*
* It supports flags like `--validate-only` and `--no-commit` for flexible control.
* @module scripts/validate-mcp-publish-schema
*/
import Ajv from "ajv";
import addFormats from "ajv-formats";
import axios from "axios";
import { execSync } from "child_process";
import fs from "fs/promises";
import path from "path";
// --- Constants ---
const PACKAGE_JSON_PATH = path.resolve(process.cwd(), "package.json");
const SERVER_JSON_PATH = path.resolve(process.cwd(), "server.json");
const MCP_SCHEMA_URL =
"https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json";
const MCP_REGISTRY_URL = "https://registry.modelcontextprotocol.io/v0/servers";
// --- Helper Functions ---
function runCommand(command: string, stepName: string) {
console.log(`\n--- š Starting Step: ${stepName} ---`);
console.log(`> ${command}`);
try {
execSync(command, { stdio: "inherit" });
console.log(`--- ā
Finished Step: ${stepName} ---`);
} catch (_error) {
console.error(`\n--- ā Step Failed: ${stepName} ---`);
console.error(`Command "${command}" failed.`);
process.exit(1);
}
}
async function verifyPublication(
serverName: string,
maxRetries = 5,
delay = 3000,
) {
const stepName = "Verify Publication";
console.log(`\n--- š Starting Step: ${stepName} ---`);
const searchUrl = `${MCP_REGISTRY_URL}?search=${serverName}`;
console.log(`Querying: ${searchUrl}`);
for (let i = 0; i < maxRetries; i++) {
try {
const response = await axios.get(searchUrl);
if (
response.data &&
response.data.servers &&
response.data.servers.length > 0
) {
console.log(
"ā
Verification successful! Server is live in the registry.",
);
console.log(`--- ā
Finished Step: ${stepName} ---`);
return;
}
} catch (error) {
if (error instanceof Error) {
console.warn(`Attempt ${i + 1} failed:`, error.message);
} else {
console.warn(`Attempt ${i + 1} failed:`, String(error));
}
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
console.error(`\n--- ā Step Failed: ${stepName} ---`);
console.error(
`Could not verify server publication after ${maxRetries} attempts.`,
);
process.exit(1);
}
async function syncMetadata(): Promise<{ version: string; mcpName: string }> {
const stepName = "Sync Metadata from package.json";
console.log(`\n--- š Starting Step: ${stepName} ---`);
try {
const pkgContent = await fs.readFile(PACKAGE_JSON_PATH, "utf-8");
const serverContent = await fs.readFile(SERVER_JSON_PATH, "utf-8");
const pkg = JSON.parse(pkgContent);
const server = JSON.parse(serverContent);
const { version, mcpName } = pkg;
if (!version || !mcpName) {
throw new Error(
"`version` and/or `mcpName` are missing from package.json.",
);
}
server.version = version;
server.mcpName = mcpName;
if (Array.isArray(server.packages)) {
server.packages.forEach((p: { version?: string }) => {
p.version = version;
});
console.log(`Updated version for ${server.packages.length} package(s).`);
}
await fs.writeFile(SERVER_JSON_PATH, JSON.stringify(server, null, 2));
console.log(`Synced server.json to version "${version}".`);
console.log(`--- ā
Finished Step: ${stepName} ---`);
return { version, mcpName };
} catch (error) {
console.error(`\n--- ā Step Failed: ${stepName} ---`, error);
process.exit(1);
}
}
function autoCommitChanges(version: string) {
const stepName = "Auto-commit server.json";
console.log(`\n--- š Starting Step: ${stepName} ---`);
try {
const status = execSync("git status --porcelain server.json")
.toString()
.trim();
if (!status) {
console.log("No changes to commit in server.json. Skipping.");
console.log(`--- ā
Finished Step: ${stepName} (No-op) ---`);
return;
}
execSync("git add server.json");
const commitMessage = `chore(release): bump server.json to v${version}`;
const commitCommand = `git commit --no-verify -m "${commitMessage}"`;
console.log(`> ${commitCommand}`);
execSync(commitCommand);
console.log("Successfully committed version bump for server.json.");
console.log(`--- ā
Finished Step: ${stepName} ---`);
} catch (_error) {
console.warn(`\n--- ā ļø Step Skipped: ${stepName} ---`);
console.warn("Failed to auto-commit. Please commit changes manually.");
}
}
async function validateServerJson() {
const stepName = "Validate server.json Schema";
console.log(`\n--- š Starting Step: ${stepName} ---`);
try {
const { data: schema } = await axios.get(MCP_SCHEMA_URL);
const serverJson = JSON.parse(await fs.readFile(SERVER_JSON_PATH, "utf-8"));
const ajv = new Ajv({ strict: false });
addFormats(ajv);
const validate = ajv.compile(schema);
if (!validate(serverJson)) {
console.error("Validation failed:", validate.errors);
throw new Error("server.json does not conform to the MCP schema.");
}
console.log("Validation successful!");
console.log(`--- ā
Finished Step: ${stepName} ---`);
} catch (error) {
console.error(`\n--- ā Step Failed: ${stepName} ---`, error);
process.exit(1);
}
}
async function main() {
const args = process.argv.slice(2);
const syncOnly = args.includes("--sync-only");
const validateOnly = args.includes("--validate-only");
const noCommit = args.includes("--no-commit");
const publishOnly = args.includes("--publish-only");
const verifyOnly = args.includes("--verify-only");
console.log("š Starting MCP Server Publish Workflow...");
if (verifyOnly) {
console.log("\nāŖ --verify-only flag detected. Skipping all other steps.");
const pkg = JSON.parse(await fs.readFile(PACKAGE_JSON_PATH, "utf-8"));
await verifyPublication(pkg.mcpName);
console.log("\nššš Verification Complete! ššš");
return;
}
if (publishOnly) {
console.log(
"\nāŖ --publish-only flag detected. Skipping local file changes.",
);
runCommand("mcp-publisher login github", "Authenticate with GitHub");
runCommand("mcp-publisher publish", "Publish to MCP Registry");
const pkg = JSON.parse(await fs.readFile(PACKAGE_JSON_PATH, "utf-8"));
await verifyPublication(pkg.mcpName);
console.log("\nššš Publish Complete! ššš");
return;
}
const { version, mcpName } = await syncMetadata();
if (syncOnly) {
console.log("\nā
--sync-only flag detected. Halting after metadata sync.");
return;
}
await validateServerJson();
if (validateOnly) {
console.log(
"\nā
--validate-only flag detected. Halting after validation.",
);
return;
}
if (!noCommit) {
autoCommitChanges(version);
} else {
console.log("\nāŖ --no-commit flag detected. Skipping auto-commit.");
}
runCommand("mcp-publisher login github", "Authenticate with GitHub");
runCommand("mcp-publisher publish", "Publish to MCP Registry");
await verifyPublication(mcpName);
console.log(
"\nššš Workflow Complete! Your server has been successfully published. ššš",
);
}
// --- Execute ---
main();