agent-comms 8.15.11 → 8.15.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,7 @@
15
15
  "source": "url",
16
16
  "url": "https://github.com/ExaDev/agent-comms.git"
17
17
  },
18
- "version": "8.15.11"
18
+ "version": "8.15.13"
19
19
  }
20
20
  ]
21
21
  }
@@ -18,5 +18,5 @@
18
18
  },
19
19
  "name": "agent-comms",
20
20
  "repository": "https://github.com/ExaDev/agent-comms",
21
- "version": "8.15.11"
21
+ "version": "8.15.13"
22
22
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-comms",
3
3
  "description": "Cross-harness communication mesh for LLM agents — rooms, DMs, presence, and real-time push delivery over TCP",
4
- "version": "8.15.11",
4
+ "version": "8.15.13",
5
5
  "bin": {
6
6
  "agent-comms": "dist/cli.js"
7
7
  },
@@ -142,7 +142,6 @@
142
142
  "mutation": "stryker run stryker.config.ts",
143
143
  "prepare": "husky || true",
144
144
  "prepush": "turbo run _prepush",
145
- "publish:mcp-registry": "tsx scripts/publish-mcp-registry.ts",
146
145
  "test": "turbo run _test",
147
146
  "test:all": "pnpm test && pnpm test:delivery && pnpm test:smoke",
148
147
  "test:delivery": "tsx src/test/delivery-receipt.runner.ts",
@@ -1,9 +0,0 @@
1
- export declare const INITIAL_RETRY_DELAY_MS = 15000;
2
- export declare const MAX_RETRY_DELAY_MS = 120000;
3
- export declare const RETRY_BACKOFF_MULTIPLIER = 2;
4
- export declare const MAX_TOTAL_RETRY_MS: number;
5
- /** The next delay in the doubling-with-cap backoff schedule, given the delay just used. */
6
- export declare function nextRetryDelayMs(previousDelayMs: number): number;
7
- /** Recognises a publish failure worth retrying (npm propagation lag, a transient 5xx from the registry's own infrastructure, a connection-level failure that never reached the registry, a login JWT that expired mid-retry-window, or the registry's database running short of resources) as opposed to any other failure -- a genuinely invalid publish request or authorisation failure, for instance, which retrying can never fix. */
8
- export declare function isRetryablePublishFailure(output: string): boolean;
9
- //# sourceMappingURL=mcp-registry-retry.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-registry-retry.d.ts","sourceRoot":"","sources":["../../src/core/mcp-registry-retry.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAC7C,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAI1C,eAAO,MAAM,kBAAkB,QAC+B,CAAC;AAE/D,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAKhE;AAuCD,2aAA2a;AAC3a,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAQjE"}
@@ -1,49 +0,0 @@
1
- // Pure retry logic for scripts/publish-mcp-registry.ts, kept here rather than in the script itself so it's testable under src/test/ -- tsconfig.json's rootDir is src/, so a test there can't import from scripts/ (outside that root), while a script importing from src/ is the normal direction. The script itself keeps the actual Date.now()/spawnSync-driven retry loop, which is inherently imperative and time-based; this module holds only the two decisions that loop makes that are worth asserting deterministically.
2
- // The MCP registry validates a publish against the npm registry, where the version semantic-release published seconds earlier may not be visible yet. The registry's own error text says to wait and retry, so retry that failure specifically instead of failing the release job on propagation lag.
3
- //
4
- // Confirmed insufficient in production (ExaDev/agent-comms, 2026-09-13, v2.18.0): the original fixed [15, 30, 60, 120] schedule (225s total) exhausted and failed the release job while npm's own CDN still hadn't propagated the just-published version -- a real, observed lag longer than that budget, not a hypothetical one. The release job carries no timeout-minutes of its own, so there's no tight external deadline forcing a small retry budget.
5
- export const INITIAL_RETRY_DELAY_MS = 15_000;
6
- export const MAX_RETRY_DELAY_MS = 120_000;
7
- export const RETRY_BACKOFF_MULTIPLIER = 2;
8
- const MAX_TOTAL_RETRY_MINUTES = 15;
9
- const SECONDS_PER_MINUTE = 60;
10
- const MS_PER_SECOND = 1000;
11
- export const MAX_TOTAL_RETRY_MS = MAX_TOTAL_RETRY_MINUTES * SECONDS_PER_MINUTE * MS_PER_SECOND;
12
- /** The next delay in the doubling-with-cap backoff schedule, given the delay just used. */
13
- export function nextRetryDelayMs(previousDelayMs) {
14
- return Math.min(previousDelayMs * RETRY_BACKOFF_MULTIPLIER, MAX_RETRY_DELAY_MS);
15
- }
16
- /** The MCP registry's own npm-propagation-lag error text -- the version semantic-release just published may not be visible to the registry's own npm lookup for a few seconds. */
17
- function isNpmPropagationLag(output) {
18
- return (output.includes("was not found") &&
19
- output.includes("A newly published release can take a moment"));
20
- }
21
- // Confirmed as a second, distinct retryable failure mode in the same run this retry logic was first fixed for (ExaDev/agent-comms, 2026-09-13, v2.18.1): the registry returned a genuine HTTP 504 (a transient gateway timeout on the registry's own infrastructure, not anything to do with npm propagation), and the retry logic at the time only recognised the propagation-lag text above, so it failed the release job immediately instead of retrying a condition that resolved itself on a plain re-run seconds later. Any 5xx is the registry's own server failing to complete the request, not this side's request being wrong -- exactly the class of error a retry can plausibly fix, unlike a 4xx (this side asked for something invalid) other than the specific propagation-lag 404 already handled above.
22
- const SERVER_ERROR_STATUS_PATTERN = /server returned status 5\d{2}\b/;
23
- function isRetryableServerError(output) {
24
- return SERVER_ERROR_STATUS_PATTERN.test(output);
25
- }
26
- // Confirmed as a third retryable failure mode (ExaDev/agent-comms, 2026-09-17, issue #180, run 35249771867, v3.9.0): the registry request never got as far as an HTTP response at all, a raw connection-level failure from mcp-publisher's own Go HTTP client (a DNS/dial/timeout/reset at the TCP layer), which matches neither the propagation-lag text nor the 5xx status pattern above since there is no status line to match against. This is transient infrastructure the same way a 5xx is: the request itself may well have been valid, retrying it can plausibly succeed.
27
- const CONNECTION_ERROR_PATTERN = /\b(?:dial tcp|i\/o timeout|connection refused|connection reset|no such host)\b/;
28
- function isConnectionError(output) {
29
- return CONNECTION_ERROR_PATTERN.test(output);
30
- }
31
- // Confirmed as a fourth retryable failure mode (ExaDev/agent-comms, 2026-09-17, issue #180, run 35239013253, v3.9.0): mcp-publisher logs in with `login github-oidc` once before the retry loop starts, and a retry attempt late enough in the MAX_TOTAL_RETRY_MS budget can present a JWT that expired in the meantime, producing a 401 that no amount of retrying the same token will ever fix. It's retryable only because the caller re-runs the login step before every retry attempt (see scripts/publish-mcp-registry.ts), which mints a fresh token, so this classifier deliberately matches only the specific "expired" 401 body, not a 401 in general, since a genuine authorisation failure (wrong scope, revoked credential) would 401 again regardless of how fresh the token is.
32
- const EXPIRED_JWT_STATUS_PATTERN = /server returned status 401\b/;
33
- function isExpiredJwt(output) {
34
- return EXPIRED_JWT_STATUS_PATTERN.test(output) && output.includes("expired");
35
- }
36
- // Confirmed as a fifth retryable failure mode (ExaDev/agent-comms, 2026-10-21, release run 39670379457, v8.15.4): the registry's own Postgres had no free connection slots (SQLSTATE 53300), and the registry reported that as an HTTP 400 "Failed to publish server" and not a 5xx, so the status pattern above never matched and the release job failed on its first attempt, before the GitHub release was created. PostgreSQL's SQLSTATE class 53 is "insufficient resources" (no connection slots, disk full, out of memory, configuration limit): a shortage on the registry's side that clears by itself and has nothing to do with the request, so it is retryable whatever HTTP status carries it. Any other SQLSTATE class (a constraint violation, say) describes the request or its data and would fail again.
37
- const DATABASE_RESOURCE_SHORTAGE_PATTERN = /\bSQLSTATE 53\d{3}\b/;
38
- function isDatabaseResourceShortage(output) {
39
- return DATABASE_RESOURCE_SHORTAGE_PATTERN.test(output);
40
- }
41
- /** Recognises a publish failure worth retrying (npm propagation lag, a transient 5xx from the registry's own infrastructure, a connection-level failure that never reached the registry, a login JWT that expired mid-retry-window, or the registry's database running short of resources) as opposed to any other failure -- a genuinely invalid publish request or authorisation failure, for instance, which retrying can never fix. */
42
- export function isRetryablePublishFailure(output) {
43
- return (isNpmPropagationLag(output) ||
44
- isRetryableServerError(output) ||
45
- isConnectionError(output) ||
46
- isExpiredJwt(output) ||
47
- isDatabaseResourceShortage(output));
48
- }
49
- //# sourceMappingURL=mcp-registry-retry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-registry-retry.js","sourceRoot":"","sources":["../../src/core/mcp-registry-retry.ts"],"names":[],"mappings":"AAAA,mgBAAmgB;AAEngB,sSAAsS;AACtS,EAAE;AACF,6bAA6b;AAC7b,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAC7C,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAC1C,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAC1C,MAAM,uBAAuB,GAAG,EAAE,CAAC;AACnC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAC9B,MAAM,aAAa,GAAG,IAAI,CAAC;AAC3B,MAAM,CAAC,MAAM,kBAAkB,GAC7B,uBAAuB,GAAG,kBAAkB,GAAG,aAAa,CAAC;AAE/D,2FAA2F;AAC3F,MAAM,UAAU,gBAAgB,CAAC,eAAuB;IACtD,OAAO,IAAI,CAAC,GAAG,CACb,eAAe,GAAG,wBAAwB,EAC1C,kBAAkB,CACnB,CAAC;AACJ,CAAC;AAED,kLAAkL;AAClL,SAAS,mBAAmB,CAAC,MAAc;IACzC,OAAO,CACL,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC;QAChC,MAAM,CAAC,QAAQ,CAAC,6CAA6C,CAAC,CAC/D,CAAC;AACJ,CAAC;AAED,yxBAAyxB;AACzxB,MAAM,2BAA2B,GAAG,iCAAiC,CAAC;AAEtE,SAAS,sBAAsB,CAAC,MAAc;IAC5C,OAAO,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAClD,CAAC;AAED,mjBAAmjB;AACnjB,MAAM,wBAAwB,GAC5B,gFAAgF,CAAC;AAEnF,SAAS,iBAAiB,CAAC,MAAc;IACvC,OAAO,wBAAwB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/C,CAAC;AAED,+vBAA+vB;AAC/vB,MAAM,0BAA0B,GAAG,8BAA8B,CAAC;AAElE,SAAS,YAAY,CAAC,MAAc;IAClC,OAAO,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC/E,CAAC;AAED,2xBAA2xB;AAC3xB,MAAM,kCAAkC,GAAG,sBAAsB,CAAC;AAElE,SAAS,0BAA0B,CAAC,MAAc;IAChD,OAAO,kCAAkC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACzD,CAAC;AAED,2aAA2a;AAC3a,MAAM,UAAU,yBAAyB,CAAC,MAAc;IACtD,OAAO,CACL,mBAAmB,CAAC,MAAM,CAAC;QAC3B,sBAAsB,CAAC,MAAM,CAAC;QAC9B,iBAAiB,CAAC,MAAM,CAAC;QACzB,YAAY,CAAC,MAAM,CAAC;QACpB,0BAA0B,CAAC,MAAM,CAAC,CACnC,CAAC;AACJ,CAAC"}
@@ -1,5 +0,0 @@
1
- /** The registry's endpoint for one version of one server. The name carries a `/` (`io.github.<owner>/<repo>`) that must stay inside a single path segment. */
2
- export declare function registryVersionUrl(name: string, version: string): string;
3
- /** Reads the status of a {@link registryVersionUrl} response: 200 means listed, 404 means not listed. Any other status says nothing about whether the version is listed, so it throws and fails the job to be re-run, never guessing either way. */
4
- export declare function isPublishedOnRegistry(status: number): boolean;
5
- //# sourceMappingURL=mcp-registry-version.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-registry-version.d.ts","sourceRoot":"","sources":["../../src/core/mcp-registry-version.ts"],"names":[],"mappings":"AAKA,8JAA8J;AAC9J,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,oPAAoP;AACpP,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM7D"}
@@ -1,17 +0,0 @@
1
- // Decides whether a version is already listed on the MCP registry, kept here rather than in scripts/publish-mcp-registry.ts for the same reason as mcp-registry-retry.ts: a test under src/test/ cannot import from scripts/. The registry publish runs as its own CI job after the release and is re-run by hand after an outage, so it must skip a version the registry already lists, because publishing one again fails the job.
2
- const REGISTRY_BASE_URL = "https://registry.modelcontextprotocol.io";
3
- const HTTP_OK = 200;
4
- const HTTP_NOT_FOUND = 404;
5
- /** The registry's endpoint for one version of one server. The name carries a `/` (`io.github.<owner>/<repo>`) that must stay inside a single path segment. */
6
- export function registryVersionUrl(name, version) {
7
- return `${REGISTRY_BASE_URL}/v0/servers/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`;
8
- }
9
- /** Reads the status of a {@link registryVersionUrl} response: 200 means listed, 404 means not listed. Any other status says nothing about whether the version is listed, so it throws and fails the job to be re-run, never guessing either way. */
10
- export function isPublishedOnRegistry(status) {
11
- if (status === HTTP_OK)
12
- return true;
13
- if (status === HTTP_NOT_FOUND)
14
- return false;
15
- throw new Error(`MCP registry answered ${String(status)} when asked whether the version is listed`);
16
- }
17
- //# sourceMappingURL=mcp-registry-version.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-registry-version.js","sourceRoot":"","sources":["../../src/core/mcp-registry-version.ts"],"names":[],"mappings":"AAAA,qaAAqa;AACra,MAAM,iBAAiB,GAAG,0CAA0C,CAAC;AACrE,MAAM,OAAO,GAAG,GAAG,CAAC;AACpB,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B,8JAA8J;AAC9J,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,OAAe;IAC9D,OAAO,GAAG,iBAAiB,eAAe,kBAAkB,CAAC,IAAI,CAAC,aAAa,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;AAC/G,CAAC;AAED,oPAAoP;AACpP,MAAM,UAAU,qBAAqB,CAAC,MAAc;IAClD,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,MAAM,KAAK,cAAc;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,IAAI,KAAK,CACb,yBAAyB,MAAM,CAAC,MAAM,CAAC,2CAA2C,CACnF,CAAC;AACJ,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=mcp-registry-retry.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-registry-retry.unit.test.d.ts","sourceRoot":"","sources":["../../src/test/mcp-registry-retry.unit.test.ts"],"names":[],"mappings":""}
@@ -1,75 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { INITIAL_RETRY_DELAY_MS, MAX_RETRY_DELAY_MS, MAX_TOTAL_RETRY_MS, isRetryablePublishFailure, nextRetryDelayMs, } from "../core/mcp-registry-retry.js";
3
- const DELAY_DOUBLING_FACTOR = 2;
4
- const MULTIPLIER_WELL_BEYOND_MAX_DELAY = 10;
5
- const MS_PER_SECOND = 1000;
6
- const LEGACY_SCHEDULE_STEP_1_SECONDS = 15;
7
- const LEGACY_SCHEDULE_STEP_2_SECONDS = 30;
8
- const LEGACY_SCHEDULE_STEP_3_SECONDS = 60;
9
- const LEGACY_SCHEDULE_STEP_4_SECONDS = 120;
10
- describe("nextRetryDelayMs", () => {
11
- it("doubles the previous delay", () => {
12
- expect(nextRetryDelayMs(INITIAL_RETRY_DELAY_MS)).toBe(INITIAL_RETRY_DELAY_MS * DELAY_DOUBLING_FACTOR);
13
- });
14
- it("caps at MAX_RETRY_DELAY_MS", () => {
15
- expect(nextRetryDelayMs(MAX_RETRY_DELAY_MS)).toBe(MAX_RETRY_DELAY_MS);
16
- expect(nextRetryDelayMs(MAX_RETRY_DELAY_MS * MULTIPLIER_WELL_BEYOND_MAX_DELAY)).toBe(MAX_RETRY_DELAY_MS);
17
- });
18
- it("accumulates to comfortably exceed the previous, confirmed-insufficient 225-second retry budget (agent-comms v2.18.0's release job exhausted a fixed [15, 30, 60, 120] schedule while npm's own CDN still hadn't propagated the just-published version)", () => {
19
- const previousBudgetMs = (LEGACY_SCHEDULE_STEP_1_SECONDS +
20
- LEGACY_SCHEDULE_STEP_2_SECONDS +
21
- LEGACY_SCHEDULE_STEP_3_SECONDS +
22
- LEGACY_SCHEDULE_STEP_4_SECONDS) *
23
- MS_PER_SECOND;
24
- let delayMs = INITIAL_RETRY_DELAY_MS;
25
- let totalMs = 0;
26
- while (totalMs < MAX_TOTAL_RETRY_MS) {
27
- totalMs += delayMs;
28
- delayMs = nextRetryDelayMs(delayMs);
29
- }
30
- expect(totalMs > previousBudgetMs).toBeTruthy();
31
- expect(totalMs >= MAX_TOTAL_RETRY_MS).toBeTruthy();
32
- });
33
- });
34
- describe("isRetryablePublishFailure", () => {
35
- it("recognises the MCP registry's own npm-propagation-lag error text", () => {
36
- const output = "registry validation failed for package 0 (agent-comms): NPM package 'agent-comms' exists, but version '2.18.0' was not found (status: 404). A newly published release can take a moment to appear on the registry. Wait and retry, or publish version '2.18.0' before registering it";
37
- expect(isRetryablePublishFailure(output)).toBe(true);
38
- });
39
- it("recognises a transient 5xx from the registry's own infrastructure -- confirmed as a real, second retryable failure mode (ExaDev/agent-comms, 2026-09-13, v2.18.1): the registry returned a genuine HTTP 504 that resolved itself on a plain re-run seconds later, but the retry logic at the time only recognised the propagation-lag text above and failed the release job immediately instead of retrying it", () => {
40
- const output = "Error: publish failed: server returned status 504: <html>";
41
- expect(isRetryablePublishFailure(output)).toBe(true);
42
- });
43
- it("recognises the registry's own database running out of connection slots, which it reports as an HTTP 400 and not a 5xx: the request was valid and the registry could not begin a transaction for it (ExaDev/agent-comms release run 39670379457, v8.15.4), so the 5xx pattern never matched and the release job failed without a single retry", () => {
44
- const output = 'Error: publish failed: server returned status 400: {"title":"Bad Request","status":400,"detail":"Failed to publish server","errors":[{"message":"failed to begin transaction: failed to connect to `user=app database=app`:\\n\\t34.118.233.51:5432 (registry-pg-rw.default): server error: FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute (SQLSTATE 53300)"}]}';
45
- expect(isRetryablePublishFailure(output)).toBe(true);
46
- });
47
- it("does not retry a database error that is not a resource shortage, such as a constraint violation, which a retry would hit again", () => {
48
- const output = 'Error: publish failed: server returned status 400: {"title":"Bad Request","status":400,"detail":"Failed to publish server","errors":[{"message":"duplicate key value violates unique constraint (SQLSTATE 23505)"}]}';
49
- expect(isRetryablePublishFailure(output)).toBe(false);
50
- });
51
- it("does not retry a 4xx other than the specific propagation-lag 404 -- a genuinely invalid publish request, which retrying can never fix", () => {
52
- const output = "Error: publish failed: server returned status 400: Bad Request";
53
- expect(isRetryablePublishFailure(output)).toBe(false);
54
- });
55
- it("does not misclassify an unrelated failure as retryable", () => {
56
- expect(isRetryablePublishFailure("permission denied")).toBe(false);
57
- });
58
- it("recognises a raw connection-level failure with no HTTP status, confirmed in production (ExaDev/agent-comms, run 35249771867, v3.9.0): a dial-tcp i/o timeout reaching the registry, which matches neither the propagation-lag text nor the 5xx status pattern above since no HTTP response was ever received", () => {
59
- const output = 'Error: publish failed: error sending request: Post "https://registry.modelcontextprotocol.io/v0/publish": dial tcp 34.61.200.254:443: i/o timeout';
60
- expect(isRetryablePublishFailure(output)).toBe(true);
61
- });
62
- it("recognises a connection-refused failure as the same class of connection-level error", () => {
63
- const output = 'Error: publish failed: error sending request: Post "https://registry.modelcontextprotocol.io/v0/publish": dial tcp 34.61.200.254:443: connect: connection refused';
64
- expect(isRetryablePublishFailure(output)).toBe(true);
65
- });
66
- it("recognises a 401 caused by the login JWT expiring mid-retry-window, confirmed in production (ExaDev/agent-comms, run 35239013253, v3.9.0): mcp-publisher logs in once before the retry loop starts, and a retry attempt late in the 15-minute budget can present a token that has since expired", () => {
67
- const output = 'Error: publish failed: server returned status 401: {"title":"Unauthorized","status":401,"detail":"Invalid or expired Registry JWT token","instance":"/v0/publish"}';
68
- expect(isRetryablePublishFailure(output)).toBe(true);
69
- });
70
- it("does not retry a 401 that is a genuine authorisation failure rather than an expired token, since retrying an unrecognised or wrongly-scoped credential can never fix it", () => {
71
- const output = 'Error: publish failed: server returned status 401: {"title":"Unauthorized","status":401,"detail":"Registry JWT token does not grant publish access to this namespace","instance":"/v0/publish"}';
72
- expect(isRetryablePublishFailure(output)).toBe(false);
73
- });
74
- });
75
- //# sourceMappingURL=mcp-registry-retry.unit.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-registry-retry.unit.test.js","sourceRoot":"","sources":["../../src/test/mcp-registry-retry.unit.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=mcp-registry-version.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-registry-version.unit.test.d.ts","sourceRoot":"","sources":["../../src/test/mcp-registry-version.unit.test.ts"],"names":[],"mappings":""}
@@ -1,22 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { isPublishedOnRegistry, registryVersionUrl, } from "../core/mcp-registry-version.js";
3
- const HTTP_OK = 200;
4
- const HTTP_NOT_FOUND = 404;
5
- const HTTP_BAD_GATEWAY = 502;
6
- describe("registryVersionUrl", () => {
7
- it("encodes the namespace separator so the server name stays one path segment", () => {
8
- expect(registryVersionUrl("io.github.ExaDev/agent-comms", "8.15.4")).toBe("https://registry.modelcontextprotocol.io/v0/servers/io.github.ExaDev%2Fagent-comms/versions/8.15.4");
9
- });
10
- });
11
- describe("isPublishedOnRegistry", () => {
12
- it("treats a 200 as published", () => {
13
- expect(isPublishedOnRegistry(HTTP_OK)).toBe(true);
14
- });
15
- it("treats a 404 as not published, which is what makes a missed publish detectable", () => {
16
- expect(isPublishedOnRegistry(HTTP_NOT_FOUND)).toBe(false);
17
- });
18
- it("refuses to guess from any other status, since publishing a version the registry already lists fails the job", () => {
19
- expect(() => isPublishedOnRegistry(HTTP_BAD_GATEWAY)).toThrow(String(HTTP_BAD_GATEWAY));
20
- });
21
- });
22
- //# sourceMappingURL=mcp-registry-version.unit.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp-registry-version.unit.test.js","sourceRoot":"","sources":["../../src/test/mcp-registry-version.unit.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,iCAAiC,CAAC;AAEzC,MAAM,OAAO,GAAG,GAAG,CAAC;AACpB,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,2EAA2E,EAAE,GAAG,EAAE;QACnF,MAAM,CAAC,kBAAkB,CAAC,8BAA8B,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CACvE,oGAAoG,CACrG,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gFAAgF,EAAE,GAAG,EAAE;QACxF,MAAM,CAAC,qBAAqB,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6GAA6G,EAAE,GAAG,EAAE;QACrH,MAAM,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAC3D,MAAM,CAAC,gBAAgB,CAAC,CACzB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -1,69 +0,0 @@
1
- // Pure retry logic for scripts/publish-mcp-registry.ts, kept here rather than in the script itself so it's testable under src/test/ -- tsconfig.json's rootDir is src/, so a test there can't import from scripts/ (outside that root), while a script importing from src/ is the normal direction. The script itself keeps the actual Date.now()/spawnSync-driven retry loop, which is inherently imperative and time-based; this module holds only the two decisions that loop makes that are worth asserting deterministically.
2
-
3
- // The MCP registry validates a publish against the npm registry, where the version semantic-release published seconds earlier may not be visible yet. The registry's own error text says to wait and retry, so retry that failure specifically instead of failing the release job on propagation lag.
4
- //
5
- // Confirmed insufficient in production (ExaDev/agent-comms, 2026-09-13, v2.18.0): the original fixed [15, 30, 60, 120] schedule (225s total) exhausted and failed the release job while npm's own CDN still hadn't propagated the just-published version -- a real, observed lag longer than that budget, not a hypothetical one. The release job carries no timeout-minutes of its own, so there's no tight external deadline forcing a small retry budget.
6
- export const INITIAL_RETRY_DELAY_MS = 15_000;
7
- export const MAX_RETRY_DELAY_MS = 120_000;
8
- export const RETRY_BACKOFF_MULTIPLIER = 2;
9
- const MAX_TOTAL_RETRY_MINUTES = 15;
10
- const SECONDS_PER_MINUTE = 60;
11
- const MS_PER_SECOND = 1000;
12
- export const MAX_TOTAL_RETRY_MS =
13
- MAX_TOTAL_RETRY_MINUTES * SECONDS_PER_MINUTE * MS_PER_SECOND;
14
-
15
- /** The next delay in the doubling-with-cap backoff schedule, given the delay just used. */
16
- export function nextRetryDelayMs(previousDelayMs: number): number {
17
- return Math.min(
18
- previousDelayMs * RETRY_BACKOFF_MULTIPLIER,
19
- MAX_RETRY_DELAY_MS,
20
- );
21
- }
22
-
23
- /** The MCP registry's own npm-propagation-lag error text -- the version semantic-release just published may not be visible to the registry's own npm lookup for a few seconds. */
24
- function isNpmPropagationLag(output: string): boolean {
25
- return (
26
- output.includes("was not found") &&
27
- output.includes("A newly published release can take a moment")
28
- );
29
- }
30
-
31
- // Confirmed as a second, distinct retryable failure mode in the same run this retry logic was first fixed for (ExaDev/agent-comms, 2026-09-13, v2.18.1): the registry returned a genuine HTTP 504 (a transient gateway timeout on the registry's own infrastructure, not anything to do with npm propagation), and the retry logic at the time only recognised the propagation-lag text above, so it failed the release job immediately instead of retrying a condition that resolved itself on a plain re-run seconds later. Any 5xx is the registry's own server failing to complete the request, not this side's request being wrong -- exactly the class of error a retry can plausibly fix, unlike a 4xx (this side asked for something invalid) other than the specific propagation-lag 404 already handled above.
32
- const SERVER_ERROR_STATUS_PATTERN = /server returned status 5\d{2}\b/;
33
-
34
- function isRetryableServerError(output: string): boolean {
35
- return SERVER_ERROR_STATUS_PATTERN.test(output);
36
- }
37
-
38
- // Confirmed as a third retryable failure mode (ExaDev/agent-comms, 2026-09-17, issue #180, run 35249771867, v3.9.0): the registry request never got as far as an HTTP response at all, a raw connection-level failure from mcp-publisher's own Go HTTP client (a DNS/dial/timeout/reset at the TCP layer), which matches neither the propagation-lag text nor the 5xx status pattern above since there is no status line to match against. This is transient infrastructure the same way a 5xx is: the request itself may well have been valid, retrying it can plausibly succeed.
39
- const CONNECTION_ERROR_PATTERN =
40
- /\b(?:dial tcp|i\/o timeout|connection refused|connection reset|no such host)\b/;
41
-
42
- function isConnectionError(output: string): boolean {
43
- return CONNECTION_ERROR_PATTERN.test(output);
44
- }
45
-
46
- // Confirmed as a fourth retryable failure mode (ExaDev/agent-comms, 2026-09-17, issue #180, run 35239013253, v3.9.0): mcp-publisher logs in with `login github-oidc` once before the retry loop starts, and a retry attempt late enough in the MAX_TOTAL_RETRY_MS budget can present a JWT that expired in the meantime, producing a 401 that no amount of retrying the same token will ever fix. It's retryable only because the caller re-runs the login step before every retry attempt (see scripts/publish-mcp-registry.ts), which mints a fresh token, so this classifier deliberately matches only the specific "expired" 401 body, not a 401 in general, since a genuine authorisation failure (wrong scope, revoked credential) would 401 again regardless of how fresh the token is.
47
- const EXPIRED_JWT_STATUS_PATTERN = /server returned status 401\b/;
48
-
49
- function isExpiredJwt(output: string): boolean {
50
- return EXPIRED_JWT_STATUS_PATTERN.test(output) && output.includes("expired");
51
- }
52
-
53
- // Confirmed as a fifth retryable failure mode (ExaDev/agent-comms, 2026-10-21, release run 39670379457, v8.15.4): the registry's own Postgres had no free connection slots (SQLSTATE 53300), and the registry reported that as an HTTP 400 "Failed to publish server" and not a 5xx, so the status pattern above never matched and the release job failed on its first attempt, before the GitHub release was created. PostgreSQL's SQLSTATE class 53 is "insufficient resources" (no connection slots, disk full, out of memory, configuration limit): a shortage on the registry's side that clears by itself and has nothing to do with the request, so it is retryable whatever HTTP status carries it. Any other SQLSTATE class (a constraint violation, say) describes the request or its data and would fail again.
54
- const DATABASE_RESOURCE_SHORTAGE_PATTERN = /\bSQLSTATE 53\d{3}\b/;
55
-
56
- function isDatabaseResourceShortage(output: string): boolean {
57
- return DATABASE_RESOURCE_SHORTAGE_PATTERN.test(output);
58
- }
59
-
60
- /** Recognises a publish failure worth retrying (npm propagation lag, a transient 5xx from the registry's own infrastructure, a connection-level failure that never reached the registry, a login JWT that expired mid-retry-window, or the registry's database running short of resources) as opposed to any other failure -- a genuinely invalid publish request or authorisation failure, for instance, which retrying can never fix. */
61
- export function isRetryablePublishFailure(output: string): boolean {
62
- return (
63
- isNpmPropagationLag(output) ||
64
- isRetryableServerError(output) ||
65
- isConnectionError(output) ||
66
- isExpiredJwt(output) ||
67
- isDatabaseResourceShortage(output)
68
- );
69
- }
@@ -1,18 +0,0 @@
1
- // Decides whether a version is already listed on the MCP registry, kept here rather than in scripts/publish-mcp-registry.ts for the same reason as mcp-registry-retry.ts: a test under src/test/ cannot import from scripts/. The registry publish runs as its own CI job after the release and is re-run by hand after an outage, so it must skip a version the registry already lists, because publishing one again fails the job.
2
- const REGISTRY_BASE_URL = "https://registry.modelcontextprotocol.io";
3
- const HTTP_OK = 200;
4
- const HTTP_NOT_FOUND = 404;
5
-
6
- /** The registry's endpoint for one version of one server. The name carries a `/` (`io.github.<owner>/<repo>`) that must stay inside a single path segment. */
7
- export function registryVersionUrl(name: string, version: string): string {
8
- return `${REGISTRY_BASE_URL}/v0/servers/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`;
9
- }
10
-
11
- /** Reads the status of a {@link registryVersionUrl} response: 200 means listed, 404 means not listed. Any other status says nothing about whether the version is listed, so it throws and fails the job to be re-run, never guessing either way. */
12
- export function isPublishedOnRegistry(status: number): boolean {
13
- if (status === HTTP_OK) return true;
14
- if (status === HTTP_NOT_FOUND) return false;
15
- throw new Error(
16
- `MCP registry answered ${String(status)} when asked whether the version is listed`,
17
- );
18
- }