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
2 changes: 1 addition & 1 deletion toolkit-docs-generator/src/diff/toolkit-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import {
buildComparableToolSignature,
extractVersion,
stableStringify,
} from "../merger/data-merger.js";
import type {
Expand All @@ -16,6 +15,7 @@ import type {
ToolDefinition,
ToolkitMetadata,
} from "../types/index.js";
import { extractVersion } from "../utils/index.js";

// ============================================================================
// Types
Expand Down
8 changes: 1 addition & 7 deletions toolkit-docs-generator/src/merger/data-merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
ToolkitMetadata,
} from "../types/index.js";
import { mapWithConcurrency } from "../utils/concurrency.js";
import { extractVersion } from "../utils/fp.js";
import {
detectMetadataChanges,
formatFreshnessWarnings,
Expand Down Expand Up @@ -340,13 +341,6 @@ export const getProviderId = (
return toolWithAuth?.auth?.providerId ?? null;
};

/**
* Extract version from fully qualified name
*/
export const extractVersion = (fullyQualifiedName: string): string => {
const parts = fullyQualifiedName.split("@");
return parts[1] ?? "0.0.0";
};
/**
* Create default metadata for toolkits not found in Design System
*/
Expand Down
15 changes: 13 additions & 2 deletions toolkit-docs-generator/src/sources/toolkit-data-source.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not a new issue in this PR, but i did notice that we do a deeper hunt for toolkit data when looking up one toolkit than we do when listing... which means the listed toolkit's data (icon, url, etc.) might differ from a single fetch toolkit's data.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { join } from "path";
import type { ToolDefinition, ToolkitMetadata } from "../types/index.js";
import { normalizeId } from "../utils/fp.js";
import { filterToolsByHighestVersion } from "../utils/version-coherence.js";
import {
type ArcadeApiSourceConfig,
createArcadeApiSource,
Expand Down Expand Up @@ -130,13 +131,14 @@ export class CombinedToolkitDataSource implements IToolkitDataSource {
}
}

// Filter tools by version if specified
// Filter tools by version if specified, otherwise keep only the highest
// version to drop stale tools from older releases that Engine still serves.
const filteredTools = version
? tools.filter((tool) => {
const toolVersion = tool.fullyQualifiedName.split("@")[1];
return toolVersion === version;
})
: tools;
: filterToolsByHighestVersion(tools);

return {
tools: filteredTools,
Expand Down Expand Up @@ -167,6 +169,15 @@ export class CombinedToolkitDataSource implements IToolkitDataSource {
metadataMap.set(metadata.id, metadata);
}

// Filter each toolkit to its highest version to drop stale
// tools from older releases that Engine still serves.
for (const [toolkitId, tools] of toolkitGroups) {
const filtered = filterToolsByHighestVersion(tools);
if (filtered !== tools) {
toolkitGroups.set(toolkitId, filtered as ToolDefinition[]);
}
}

// Combine into ToolkitData map.
// Use getToolkitMetadata for toolkits without a direct match so that
// fallback logic (e.g. "WeaviateApi" → "Weaviate") is applied consistently,
Expand Down
9 changes: 9 additions & 0 deletions toolkit-docs-generator/src/utils/fp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ export const deepMerge = <T extends object>(
return result;
};

/**
* Extract version from a fully qualified tool name.
* "Github.CreateIssue@3.1.3" → "3.1.3"
*/
export const extractVersion = (fullyQualifiedName: string): string => {
const parts = fullyQualifiedName.split("@");
return parts[1] ?? "0.0.0";
};

/**
* Normalize a string for comparison (lowercase, remove hyphens/underscores)
*/
Expand Down
4 changes: 4 additions & 0 deletions toolkit-docs-generator/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export { readIgnoreList } from "./ignore-list.js";
export * from "./logger.js";
export * from "./progress.js";
export * from "./retry.js";
export {
filterToolsByHighestVersion,
getHighestVersion,
} from "./version-coherence.js";
87 changes: 87 additions & 0 deletions toolkit-docs-generator/src/utils/version-coherence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { ToolDefinition } from "../types/index.js";
import { extractVersion } from "./fp.js";

/**
* Parse the numeric MAJOR.MINOR.PATCH tuple from a semver string,
* stripping pre-release (`-alpha.1`) and build metadata (`+build.456`).
*
* Handles formats produced by arcade-mcp's normalize_version():
* "3.1.3", "1.2.3-beta.1", "1.2.3+build.456", "1.2.3-rc.1+build.789"
*/
const parseNumericVersion = (version: string): number[] => {
// Strip build metadata (after +) then pre-release (after -)
const core = version.split("+")[0]?.split("-")[0] ?? version;
return core.split(".").map((s) => {
const n = Number(s);
return Number.isNaN(n) ? 0 : n;
});
};

/**
* Compare two semver version strings numerically by MAJOR.MINOR.PATCH.
* Pre-release and build metadata are ignored for ordering purposes
* (they are unlikely to appear in Engine API responses, but we handle
* them defensively since arcade-mcp's semver allows them).
*
* Returns a positive number if a > b, negative if a < b, 0 if equal.
*/
const compareVersions = (a: string, b: string): number => {
const aParts = parseNumericVersion(a);
const bParts = parseNumericVersion(b);
const len = Math.max(aParts.length, bParts.length);
for (let i = 0; i < len; i++) {
const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
if (diff !== 0) return diff;
}
return 0;
};

/**
* Find the highest version among all tools in a toolkit.
* This is the version we keep — stale tools from older releases are dropped.
*/
export const getHighestVersion = (
tools: readonly ToolDefinition[]
): string | null => {
if (tools.length === 0) {
return null;
}

let best = "";
for (const tool of tools) {
const version = extractVersion(tool.fullyQualifiedName);
if (best === "" || compareVersions(version, best) > 0) {
best = version;
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

return best || null;
};
Comment thread
jottakka marked this conversation as resolved.

/**
* Keep only tools whose @version matches the highest version for
* their toolkit. If all tools share the same version (the common
* case), returns the original array unchanged.
*
* This drops stale tools from older releases that Engine still serves,
* while always preserving the newest version — even if it has fewer tools
* (e.g. tools were removed/consolidated in the new release).
*/
export const filterToolsByHighestVersion = (
tools: readonly ToolDefinition[]
): readonly ToolDefinition[] => {
const highest = getHighestVersion(tools);
if (highest === null) {
return tools;
}

// Fast path: if every tool is already at the highest version, skip filtering
const allSame = tools.every(
(t) => extractVersion(t.fullyQualifiedName) === highest
);
if (allSame) {
return tools;
}

return tools.filter((t) => extractVersion(t.fullyQualifiedName) === highest);
};
2 changes: 1 addition & 1 deletion toolkit-docs-generator/tests/merger/data-merger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
computeAllScopes,
DataMerger,
determineAuthType,
extractVersion,
getProviderId,
groupToolsByToolkit,
mergeToolkit,
Expand All @@ -32,6 +31,7 @@ import type {
ToolDefinition,
ToolkitMetadata,
} from "../../src/types/index.js";
import { extractVersion } from "../../src/utils/index.js";

// ============================================================================
// Test Fixtures - Realistic data matching production schema
Expand Down
Loading
Loading