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
51 changes: 51 additions & 0 deletions registry/server/lib/supabase-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,57 @@ export async function upsertServers(
}
}

/**
* Get available tags and categories from all servers
*/
export async function getAvailableFilters(client: SupabaseClient): Promise<{
tags: Array<{ value: string; count: number }>;
categories: Array<{ value: string; count: number }>;
}> {
// Get all latest servers with their tags and categories
const { data, error } = await client
.from("mcp_servers")
.select("tags, categories")
.eq("is_latest", true)
.eq("unlisted", false);

if (error) {
throw new Error(`Error fetching available filters: ${error.message}`);
}

const servers = (data || []) as Array<{
tags: string[] | null;
categories: string[] | null;
}>;

// Count tags
const tagCounts = new Map<string, number>();
servers.forEach((server) => {
server.tags?.forEach((tag) => {
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
});
});

// Count categories
const categoryCounts = new Map<string, number>();
servers.forEach((server) => {
server.categories?.forEach((category) => {
categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1);
});
});

// Convert to sorted arrays
const tags = Array.from(tagCounts.entries())
.map(([value, count]) => ({ value, count }))
.sort((a, b) => b.count - a.count); // Sort by count desc

const categories = Array.from(categoryCounts.entries())
.map(([value, count]) => ({ value, count }))
.sort((a, b) => b.count - a.count); // Sort by count desc

return { tags, categories };
}

/**
* Get server count by status
*/
Expand Down
2 changes: 2 additions & 0 deletions registry/server/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import {
createListRegistryTool,
createGetRegistryTool,
createVersionsRegistryTool,
createFiltersRegistryTool,
} from "./registry-binding.ts";

export const tools = [
createListRegistryTool,
createGetRegistryTool,
createVersionsRegistryTool,
createFiltersRegistryTool,
];
91 changes: 87 additions & 4 deletions registry/server/tools/registry-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
listServers as listServersFromSupabase,
getServer as getServerFromSupabase,
getServerVersions as getServerVersionsFromSupabase,
getAvailableFilters as getAvailableFiltersFromSupabase,
} from "../lib/supabase-client.ts";

// ============================================================================
Expand Down Expand Up @@ -86,10 +87,29 @@ const ListInputSchema = z
.max(100)
.default(30)
.describe("Number of items per page (default: 30)"),

where: WhereSchema.optional().describe(
"Standard WhereExpression filter (converted to simple search internally)",
),
tags: z
.array(z.string())
.optional()
.describe(
"Filter by tags (returns servers that have ANY of the specified tags)",
),
categories: z
.array(z.string())
.optional()
.describe(
"Filter by categories (returns servers that have ANY of the specified categories). Valid categories: productivity, development, data, ai, communication, infrastructure, security, monitoring, analytics, automation",
),
verified: z
.boolean()
.optional()
.describe("Filter by verification status (true = verified only)"),
hasRemote: z
.boolean()
.optional()
.describe("Filter servers that support remote execution"),
})
.describe("Filtering, sorting, and pagination context");

Expand Down Expand Up @@ -214,15 +234,23 @@ export const createListRegistryTool = (_env: Env) =>
createPrivateTool({
id: "COLLECTION_REGISTRY_APP_LIST",
description:
"Lists MCP servers available in the registry with support for pagination, search, and boolean filters (has_remotes, has_packages, is_latest, etc.)",
"Lists MCP servers available in the registry with support for pagination, search, and filters (tags, categories, verified, hasRemote). Always returns the latest version of each server.",
inputSchema: ListInputSchema,
outputSchema: ListOutputSchema,
execute: async ({
context,
}: {
context: z.infer<typeof ListInputSchema>;
}) => {
const { limit = 30, cursor, where } = context;
const {
limit = 30,
cursor,
where,
tags,
categories,
verified,
hasRemote,
} = context;
try {
// Get configuration from environment
const supabaseUrl = process.env.SUPABASE_URL;
Expand All @@ -245,7 +273,10 @@ export const createListRegistryTool = (_env: Env) =>
limit,
offset,
search: apiSearch,
hasRemote: true, // Only show servers with remotes
tags,
categories,
verified,
hasRemote: hasRemote ?? true, // Default: only show servers with remotes
});

const items = result.servers.map((server) => ({
Expand Down Expand Up @@ -391,3 +422,55 @@ export const createVersionsRegistryTool = (_env: Env) =>
}
},
});

/**
* COLLECTION_REGISTRY_APP_FILTERS - Get available filter options
*/
export const createFiltersRegistryTool = (_env: Env) =>
createPrivateTool({
id: "COLLECTION_REGISTRY_APP_FILTERS",
description:
"Gets all available tags and categories that can be used to filter MCP servers, with counts showing how many servers use each filter value",
inputSchema: z.object({}),
outputSchema: z.object({
tags: z
.array(
z.object({
value: z.string().describe("Tag name"),
count: z.number().describe("Number of servers with this tag"),
}),
)
.describe("Available tags sorted by usage count (descending)"),
categories: z
.array(
z.object({
value: z.string().describe("Category name"),
count: z.number().describe("Number of servers in this category"),
}),
)
.describe("Available categories sorted by usage count (descending)"),
}),
execute: async () => {
try {
// Get configuration from environment
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_ANON_KEY;

if (!supabaseUrl || !supabaseKey) {
throw new Error(
"Supabase not configured. Please set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.",
);
}

// Query directly from Supabase
const client = createSupabaseClient(supabaseUrl, supabaseKey);
const filters = await getAvailableFiltersFromSupabase(client);

return filters;
} catch (error) {
throw new Error(
`Error getting available filters: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
},
});
Loading