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
5 changes: 5 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi";
import type { Context } from "hono";
import { configRoutes } from "../domains/config/config-route";
import { repository } from "../domains/package/package-handler";
import { packageRoutes } from "../domains/package/package-route";
import { initRegistryFactory } from "../domains/registry/registry-factory";
import { searchRoutes } from "../domains/search/search-route";
import { SearchSO } from "../domains/search/search-so";
import { getServerPort, isSearchEnabled } from "../shared/config/environment";
import { getDirname } from "../shared/utils";

// Initialize Registry Factory with the local repository
initRegistryFactory(repository);

const initializeSearchService = async () => {
try {
await SearchSO.getInstance();
Expand Down
2 changes: 1 addition & 1 deletion src/domains/package/package-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { PackageSO } from "./package-so";
const __dirname = getDirname(import.meta.url);

const packagesDir = path.join(__dirname, "../../../packages");
const repository = new PackageRepository(packagesDir);
export const repository = new PackageRepository(packagesDir);

export const packageHandler = {
getPackageDetail: async (packageName: string, sandboxProvider?: MCPSandboxProvider) => {
Expand Down
31 changes: 26 additions & 5 deletions src/domains/package/package-so.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ToolExecutor } from "../executor/executor-types";
import { initRegistryFactory, resetRegistryFactory } from "../registry/registry-factory";
import type { PackageRepository } from "./package-repository";
import { PackageSO } from "./package-so";
import type { MCPServerPackageConfig } from "./package-types";
Expand All @@ -10,13 +11,19 @@ describe("PackageSO", () => {
let mockExecutor: ToolExecutor;

beforeEach(() => {
// Reset factory before each test
resetRegistryFactory();

// Mock PackageRepository
mockRepository = {
getPackageConfig: vi.fn(),
getAllPackages: vi.fn(),
exists: vi.fn(),
} as unknown as PackageRepository;

// Initialize Registry Factory with mock repository
initRegistryFactory(mockRepository);

// Mock ToolExecutor
mockExecutor = {
listTools: vi.fn(),
Expand All @@ -41,6 +48,7 @@ describe("PackageSO", () => {
validated: true,
};

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({
[packageName]: mockPackageInfo,
Expand All @@ -55,6 +63,7 @@ describe("PackageSO", () => {
expect(packageSO.description).toBe("A server for filesystem operations");
expect(packageSO.category).toBe("filesystem");
expect(packageSO.validated).toBe(true);
expect(mockRepository.exists).toHaveBeenCalledWith(packageName);
expect(mockRepository.getPackageConfig).toHaveBeenCalledWith(packageName);
expect(mockRepository.getAllPackages).toHaveBeenCalled();
});
Expand All @@ -70,6 +79,7 @@ describe("PackageSO", () => {
description: "A new package",
};

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});

Expand All @@ -94,6 +104,7 @@ describe("PackageSO", () => {
description: null,
} as unknown as MCPServerPackageConfig;

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});

Expand Down Expand Up @@ -140,6 +151,7 @@ describe("PackageSO", () => {
},
];

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});
vi.spyOn(mockExecutor, "listTools").mockResolvedValue(mockTools);
Expand Down Expand Up @@ -168,6 +180,7 @@ describe("PackageSO", () => {
};
const errorMessage = "Failed to list tools";

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});
vi.spyOn(mockExecutor, "listTools").mockRejectedValue(new Error(errorMessage));
Expand Down Expand Up @@ -196,6 +209,7 @@ describe("PackageSO", () => {
description: "Test description",
};

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});
vi.spyOn(mockExecutor, "executeTool").mockResolvedValue(mockResult);
Expand Down Expand Up @@ -231,6 +245,7 @@ describe("PackageSO", () => {
description: "Test description",
};

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});
vi.spyOn(mockExecutor, "executeTool").mockResolvedValue(mockResult);
Expand Down Expand Up @@ -265,6 +280,7 @@ describe("PackageSO", () => {
description: "Test description",
};

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});
vi.spyOn(mockExecutor, "executeTool").mockRejectedValue(new Error(errorMessage));
Expand Down Expand Up @@ -303,6 +319,7 @@ describe("PackageSO", () => {
},
];

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({
[packageName]: mockPackageInfo,
Expand All @@ -316,11 +333,11 @@ describe("PackageSO", () => {

// Assert
expect(detail).toEqual({
type: "mcp-server",
runtime: "node",
name: "Test Package",
packageName: "@test/package",
description: "A test package for demonstration",
category: "testing",
validated: true,
tools: mockTools,
});
});
Expand All @@ -336,6 +353,7 @@ describe("PackageSO", () => {
description: "Test description",
};

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});
vi.spyOn(mockExecutor, "listTools").mockRejectedValue(new Error("Failed to get tools"));
Expand Down Expand Up @@ -371,6 +389,7 @@ describe("PackageSO", () => {
};
const mockTools: Tool[] = [];

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});
vi.spyOn(mockExecutor, "listTools").mockResolvedValue(mockTools);
Expand All @@ -382,12 +401,12 @@ describe("PackageSO", () => {

// Assert
expect(detail).toEqual({
type: "mcp-server",
runtime: "node",
name: "Minimal Package",
packageName: "@test/minimal-package",
description: "A minimal package",
category: undefined,
validated: undefined,
tools: mockTools,
tools: [],
});
});
});
Expand All @@ -409,6 +428,7 @@ describe("PackageSO", () => {
validated: false,
};

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({
[packageName]: mockPackageInfo,
Expand Down Expand Up @@ -437,6 +457,7 @@ describe("PackageSO", () => {
description: "Test description",
};

vi.spyOn(mockRepository, "exists").mockReturnValue(true);
vi.spyOn(mockRepository, "getPackageConfig").mockReturnValue(mockConfig);
vi.spyOn(mockRepository, "getAllPackages").mockReturnValue({});

Expand Down
16 changes: 13 additions & 3 deletions src/domains/package/package-so.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import type { ToolExecutor } from "../executor/executor-types";
import { getRegistryProvider } from "../registry/registry-factory";
import type { IRegistryProvider } from "../registry/registry-types";
import type { PackageRepository } from "./package-repository";
import type { MCPServerPackageConfig, MCPServerPackageConfigWithTools } from "./package-types";

Expand All @@ -8,7 +10,6 @@ export class PackageSO {
private readonly _packageName: string,
private readonly _config: MCPServerPackageConfig,
private readonly _packageInfo: { category?: string; validated?: boolean },
_repository: PackageRepository,
private readonly _executor: ToolExecutor,
) {}

Expand Down Expand Up @@ -36,10 +37,19 @@ export class PackageSO {
repository: PackageRepository,
executor: ToolExecutor,
): Promise<PackageSO> {
const config = repository.getPackageConfig(packageName);
// Use FederatedRegistryProvider, which checks for local packages first and falls back to the official registry if not found locally.
const provider: IRegistryProvider = getRegistryProvider("FEDERATED");
const config = await provider.getPackageConfig(packageName);

if (!config) {
throw new Error(`Package '${packageName}' not found`);
}

// Get package metadata (category, validated) from local repository index if available.
const allPackages = repository.getAllPackages();
const packageInfo = allPackages[packageName] || {};
return new PackageSO(packageName, config, packageInfo, repository, executor);

return new PackageSO(packageName, config, packageInfo, executor);
}

async getTools(): Promise<Tool[]> {
Expand Down
162 changes: 162 additions & 0 deletions src/domains/registry/__tests__/federated-registry-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { MCPServerPackageConfig } from "../../package/package-types";
import { FederatedRegistryProvider } from "../providers/federated-registry-provider";
import type { LocalRegistryProvider } from "../providers/local-registry-provider";
import type { OfficialRegistryProvider } from "../providers/official-registry-provider";

describe("FederatedRegistryProvider", () => {
let mockLocalProvider: LocalRegistryProvider;
let mockOfficialProvider: OfficialRegistryProvider;
let provider: FederatedRegistryProvider;

beforeEach(() => {
// Mock LocalRegistryProvider
mockLocalProvider = {
getPackageConfig: vi.fn(),
exists: vi.fn(),
} as unknown as LocalRegistryProvider;

// Mock OfficialRegistryProvider
mockOfficialProvider = {
getPackageConfig: vi.fn(),
exists: vi.fn(),
search: vi.fn(),
} as unknown as OfficialRegistryProvider;

provider = new FederatedRegistryProvider(mockLocalProvider, mockOfficialProvider);
});

describe("getPackageConfig", () => {
it("should return local config when package exists locally", async () => {
// Arrange
const packageName = "@modelcontextprotocol/server-filesystem";
const mockConfig: MCPServerPackageConfig = {
type: "mcp-server",
runtime: "node",
packageName,
name: "Filesystem Server",
description: "A server for filesystem operations",
};

vi.spyOn(mockLocalProvider, "getPackageConfig").mockResolvedValue(mockConfig);

// Act
const result = await provider.getPackageConfig(packageName);

// Assert
expect(result).toEqual(mockConfig);
expect(mockLocalProvider.getPackageConfig).toHaveBeenCalledWith(packageName);
expect(mockOfficialProvider.getPackageConfig).not.toHaveBeenCalled();
});

it("should query official provider when local returns null", async () => {
// Arrange
const packageName = "@toolsdk.ai/tavily-mcp";
const officialConfig: MCPServerPackageConfig = {
type: "mcp-server",
runtime: "node",
packageName,
name: "Tavily MCP Server",
description: "MCP server for Tavily search",
};

vi.spyOn(mockLocalProvider, "getPackageConfig").mockResolvedValue(null);
vi.spyOn(mockOfficialProvider, "getPackageConfig").mockResolvedValue(officialConfig);

// Act
const result = await provider.getPackageConfig(packageName);

// Assert
expect(result).toEqual(officialConfig);
expect(mockLocalProvider.getPackageConfig).toHaveBeenCalledWith(packageName);
expect(mockOfficialProvider.getPackageConfig).toHaveBeenCalledWith(packageName);
});

it("should return null when both providers return null", async () => {
// Arrange
const packageName = "non-existent-package";

vi.spyOn(mockLocalProvider, "getPackageConfig").mockResolvedValue(null);
vi.spyOn(mockOfficialProvider, "getPackageConfig").mockResolvedValue(null);

// Act
const result = await provider.getPackageConfig(packageName);

// Assert
expect(result).toBeNull();
});

it("should return null when official provider throws error", async () => {
// Arrange
const packageName = "@toolsdk.ai/tavily-mcp";

vi.spyOn(mockLocalProvider, "getPackageConfig").mockResolvedValue(null);
vi.spyOn(mockOfficialProvider, "getPackageConfig").mockRejectedValue(
new Error("Network error"),
);

// Act
const result = await provider.getPackageConfig(packageName);

// Assert
expect(result).toBeNull();
});
});

describe("exists", () => {
it("should return true when package exists locally", async () => {
// Arrange
const packageName = "@modelcontextprotocol/server-filesystem";
vi.spyOn(mockLocalProvider, "exists").mockResolvedValue(true);

// Act
const result = await provider.exists(packageName);

// Assert
expect(result).toBe(true);
expect(mockLocalProvider.exists).toHaveBeenCalledWith(packageName);
expect(mockOfficialProvider.exists).not.toHaveBeenCalled();
});

it("should check official provider when local returns false", async () => {
// Arrange
const packageName = "@toolsdk.ai/tavily-mcp";
vi.spyOn(mockLocalProvider, "exists").mockResolvedValue(false);
vi.spyOn(mockOfficialProvider, "exists").mockResolvedValue(true);

// Act
const result = await provider.exists(packageName);

// Assert
expect(result).toBe(true);
expect(mockLocalProvider.exists).toHaveBeenCalledWith(packageName);
expect(mockOfficialProvider.exists).toHaveBeenCalledWith(packageName);
});

it("should return false when both providers return false", async () => {
// Arrange
const packageName = "non-existent-package";
vi.spyOn(mockLocalProvider, "exists").mockResolvedValue(false);
vi.spyOn(mockOfficialProvider, "exists").mockResolvedValue(false);

// Act
const result = await provider.exists(packageName);

// Assert
expect(result).toBe(false);
});

it("should return false when official provider throws error", async () => {
// Arrange
const packageName = "@toolsdk.ai/tavily-mcp";
vi.spyOn(mockLocalProvider, "exists").mockResolvedValue(false);
vi.spyOn(mockOfficialProvider, "exists").mockRejectedValue(new Error("Network error"));

// Act
const result = await provider.exists(packageName);

// Assert
expect(result).toBe(false);
});
});
});
Loading