From 07e5887dc925576bf315ecf7dcfff50555d994e0 Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Fri, 4 Jul 2025 19:08:22 -0500 Subject: [PATCH] feat: add project.tags and filtering based on tags for public projects --- .../api-v1/projects/list-public-projects.ts | 30 +++++++-- apps/api/src/api-v1/projects/schemas.ts | 11 ++++ apps/api/src/db/schema/deployment.ts | 2 +- apps/api/src/db/schema/project.ts | 5 ++ .../src/app/marketplace/marketplace-index.tsx | 64 +++++++++++++++---- apps/web/src/components/public-project.tsx | 8 +-- packages/types/src/openapi.d.ts | 3 + todo.md | 12 ++-- 8 files changed, 104 insertions(+), 31 deletions(-) diff --git a/apps/api/src/api-v1/projects/list-public-projects.ts b/apps/api/src/api-v1/projects/list-public-projects.ts index 6e54205d..6584d89d 100644 --- a/apps/api/src/api-v1/projects/list-public-projects.ts +++ b/apps/api/src/api-v1/projects/list-public-projects.ts @@ -4,14 +4,24 @@ import type { DefaultHonoEnv } from '@agentic/platform-hono' import { parseZodSchema } from '@agentic/platform-core' import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi' -import { and, db, eq, isNotNull, schema } from '@/db' +import { + and, + arrayContains, + db, + eq, + isNotNull, + isNull, + not, + or, + schema +} from '@/db' import { setPublicCacheControl } from '@/lib/cache-control' import { openapiAuthenticatedSecuritySchemas, openapiErrorResponses } from '@/lib/openapi-utils' -import { paginationAndPopulateProjectSchema } from './schemas' +import { listPublicProjectsQuerySchema } from './schemas' const route = createRoute({ description: @@ -22,7 +32,7 @@ const route = createRoute({ path: 'projects/public', security: openapiAuthenticatedSecuritySchemas, request: { - query: paginationAndPopulateProjectSchema + query: listPublicProjectsQuerySchema }, responses: { 200: { @@ -44,14 +54,24 @@ export function registerV1ListPublicProjects(app: OpenAPIHono) { limit = 10, sort = 'desc', sortBy = 'createdAt', - populate = [] + populate = [], + tag, + notTag } = c.req.valid('query') const projects = await db.query.projects.findMany({ // List projects that are not private and have at least one published deployment + // And optionally match a given tag where: and( eq(schema.projects.private, false), - isNotNull(schema.projects.lastPublishedDeploymentId) + isNotNull(schema.projects.lastPublishedDeploymentId), + tag ? arrayContains(schema.projects.tags, [tag]) : undefined, + notTag + ? or( + not(arrayContains(schema.projects.tags, [notTag])), + isNull(schema.projects.tags) + ) + : undefined ), with: { lastPublishedDeployment: true, diff --git a/apps/api/src/api-v1/projects/schemas.ts b/apps/api/src/api-v1/projects/schemas.ts index a3049d03..bdb12830 100644 --- a/apps/api/src/api-v1/projects/schemas.ts +++ b/apps/api/src/api-v1/projects/schemas.ts @@ -37,6 +37,11 @@ export const projectIdentifierQuerySchema = z.object({ projectIdentifier: projectIdentifierSchema }) +export const filterPublicProjectSchema = z.object({ + tag: z.string().optional(), + notTag: z.string().optional() +}) + export const populateProjectSchema = z.object({ populate: z .union([projectRelationsSchema, z.array(projectRelationsSchema)]) @@ -54,3 +59,9 @@ export const paginationAndPopulateProjectSchema = z.object({ ...paginationSchema.shape, ...populateProjectSchema.shape }) + +export const listPublicProjectsQuerySchema = z.object({ + ...paginationSchema.shape, + ...populateProjectSchema.shape, + ...filterPublicProjectSchema.shape +}) diff --git a/apps/api/src/db/schema/deployment.ts b/apps/api/src/db/schema/deployment.ts index b8a474b2..afdca376 100644 --- a/apps/api/src/db/schema/deployment.ts +++ b/apps/api/src/db/schema/deployment.ts @@ -73,7 +73,7 @@ export const deployments = pgTable( name: projectName().notNull(), description: text().default('').notNull(), - readme: text().default('').notNull(), + readme: text(), // URL to uploaded markdown document iconUrl: text(), sourceUrl: text(), homepageUrl: text(), diff --git a/apps/api/src/db/schema/project.ts b/apps/api/src/db/schema/project.ts index 114383a5..6bb38083 100644 --- a/apps/api/src/db/schema/project.ts +++ b/apps/api/src/db/schema/project.ts @@ -85,6 +85,9 @@ export const projects = pgTable( // visible on the marketplace. private: boolean().default(true).notNull(), + // Admin-controlled tags for organizing and featuring on the marketplace + tags: text().array(), + // TODO: allow for multiple aliases like vercel // alias: text(), @@ -172,6 +175,7 @@ export const projects = pgTable( index('project_teamId_idx').on(table.teamId), // index('project_alias_idx').on(table.alias), index('project_private_idx').on(table.private), + index('project_tags_idx').on(table.tags), index('project_lastPublishedDeploymentId_idx').on( table.lastPublishedDeploymentId ), @@ -215,6 +219,7 @@ export const projectSelectBaseSchema = createSelectSchema(projects, { identifier: projectIdentifierSchema, name: agenticProjectConfigSchema.shape.name, slug: agenticProjectConfigSchema.shape.slug, + tags: z.array(z.string()).optional(), lastPublishedDeploymentId: deploymentIdSchema.optional(), lastDeploymentId: deploymentIdSchema.optional(), diff --git a/apps/web/src/app/marketplace/marketplace-index.tsx b/apps/web/src/app/marketplace/marketplace-index.tsx index 04141b34..f1c2f187 100644 --- a/apps/web/src/app/marketplace/marketplace-index.tsx +++ b/apps/web/src/app/marketplace/marketplace-index.tsx @@ -8,11 +8,29 @@ import { LoadingIndicator } from '@/components/loading-indicator' import { PageContainer } from '@/components/page-container' import { PublicProject } from '@/components/public-project' import { SupplySideCTA } from '@/components/supply-side-cta' -import { useInfiniteQuery } from '@/lib/query-client' +import { useInfiniteQuery, useQuery } from '@/lib/query-client' export function MarketplaceIndex() { const ctx = useAgentic() const limit = 10 + + const { + data: featuredProjects, + isLoading: isFeaturedProjectsLoading, + isError: isFeaturedProjectsError + } = useQuery({ + queryKey: ['featured-public-projects'], + queryFn: () => + ctx!.api.listPublicProjects({ + populate: ['lastPublishedDeployment'], + limit: 3, + tag: 'featured', + sortBy: 'createdAt', + sort: 'asc' + }), + enabled: !!ctx + }) + const { data, isLoading, @@ -21,13 +39,14 @@ export function MarketplaceIndex() { fetchNextPage, isFetchingNextPage } = useInfiniteQuery({ - queryKey: ['projects'], + queryKey: ['public-projects'], queryFn: ({ pageParam = 0 }) => ctx!.api .listPublicProjects({ populate: ['lastPublishedDeployment'], offset: pageParam, - limit + limit, + notTag: 'featured' }) .then(async (projects) => { return { @@ -55,7 +74,7 @@ export function MarketplaceIndex() { return ( -
+

- {!ctx || isLoading ? ( - - ) : ( -
-

Public Projects

+
+
+

Featured

+ + {isFeaturedProjectsError ? ( +

Error fetching featured projects

+ ) : isFeaturedProjectsLoading ? ( + + ) : !featuredProjects?.length ? ( +

+ No projects found. This is likely an issue on Agentic's side. + Please refresh or contact support. +

+ ) : ( +
+ {featuredProjects.map((project) => ( + + ))} +
+ )} +
+ +
+

General

{isError ? (

Error fetching projects

+ ) : isLoading ? ( + ) : !projects.length ? (

- No projects found. This is likely an error on Agentic's side. + No projects found. This is likely an issue on Agentic's side. Please refresh or contact support.

) : ( @@ -90,7 +130,7 @@ export function MarketplaceIndex() {
)}
- )} +

{/* CTA section */} @@ -99,7 +139,7 @@ export function MarketplaceIndex() { Your API → Paid MCP, Instantly -
+
Run one command to turn any MCP server or OpenAPI service into a paid MCP product. With built-in support for every major LLM SDK and MCP client. diff --git a/apps/web/src/components/public-project.tsx b/apps/web/src/components/public-project.tsx index 4b53ffa8..63ab5c6c 100644 --- a/apps/web/src/components/public-project.tsx +++ b/apps/web/src/components/public-project.tsx @@ -36,11 +36,9 @@ export function PublicProject({ project }: { project: Project }) {
- {deployment.description && ( -

- {deployment.description} -

- )} +

+ {deployment.description} +

{project.lastPublishedDeployment && (
diff --git a/packages/types/src/openapi.d.ts b/packages/types/src/openapi.d.ts index b81f32a0..40990a8a 100644 --- a/packages/types/src/openapi.d.ts +++ b/packages/types/src/openapi.d.ts @@ -622,6 +622,7 @@ export interface components { /** @description Unique project slug. Must be ascii-only, lower-case, and kebab-case with no spaces between 1 and 256 characters. If not provided, it will be derived by slugifying `name`. */ slug?: string; private: boolean; + tags?: string[]; /** @description User id (e.g. "user_tz4a98xxat96iws9zmbrgj3a") */ userId: string; /** @description Team id (e.g. "team_tz4a98xxat96iws9zmbrgj3a") */ @@ -1364,6 +1365,8 @@ export interface operations { sort?: "asc" | "desc"; sortBy?: "createdAt" | "updatedAt"; populate?: ("user" | "team" | "lastPublishedDeployment" | "lastDeployment") | ("user" | "team" | "lastPublishedDeployment" | "lastDeployment")[]; + tag?: string; + notTag?: string; }; header?: never; path?: never; diff --git a/todo.md b/todo.md index 80e833a2..3d1e6de6 100644 --- a/todo.md +++ b/todo.md @@ -18,13 +18,12 @@ - docs: add notes about constraints on mcp origin servers (static tools) - **api keys should go beyond 1:1 consumers** - **currently not obvious how to get api key** -- marketplace public project page - - mcp inspector - - **add support to example-usage for api keys** - - add last published date somewhere +- marketplace public project detail page - add breadcrumb nav: marketplace > @agentic > search - - tool input/output schemas; move `$schema` to top + - add last published date somewhere + - tool input/output schemas; move `$schema` to the top - break out into a few subcomponents; some can be server components + - mcp inspector - improve private project page - link to public page if published - list deployment versions @@ -34,9 +33,6 @@ - marketplace index page - add disclaimer about public beta - add search / sorting - - **improve marketplace default sorting** - - what's the best way to implement this? - - add admin-based tags for main page layout (featured, etc) - replace render for api and/or add turbo for caching (too slow to deploy) - create slack + notifications - consider changing homepage hero CTA to include publishing