feat: add project.tags and filtering based on tags for public projects

pull/721/head
Travis Fischer 2025-07-04 19:08:22 -05:00
rodzic de80f97d93
commit 07e5887dc9
8 zmienionych plików z 104 dodań i 31 usunięć

Wyświetl plik

@ -4,14 +4,24 @@ import type { DefaultHonoEnv } from '@agentic/platform-hono'
import { parseZodSchema } from '@agentic/platform-core' import { parseZodSchema } from '@agentic/platform-core'
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi' 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 { setPublicCacheControl } from '@/lib/cache-control'
import { import {
openapiAuthenticatedSecuritySchemas, openapiAuthenticatedSecuritySchemas,
openapiErrorResponses openapiErrorResponses
} from '@/lib/openapi-utils' } from '@/lib/openapi-utils'
import { paginationAndPopulateProjectSchema } from './schemas' import { listPublicProjectsQuerySchema } from './schemas'
const route = createRoute({ const route = createRoute({
description: description:
@ -22,7 +32,7 @@ const route = createRoute({
path: 'projects/public', path: 'projects/public',
security: openapiAuthenticatedSecuritySchemas, security: openapiAuthenticatedSecuritySchemas,
request: { request: {
query: paginationAndPopulateProjectSchema query: listPublicProjectsQuerySchema
}, },
responses: { responses: {
200: { 200: {
@ -44,14 +54,24 @@ export function registerV1ListPublicProjects(app: OpenAPIHono<DefaultHonoEnv>) {
limit = 10, limit = 10,
sort = 'desc', sort = 'desc',
sortBy = 'createdAt', sortBy = 'createdAt',
populate = [] populate = [],
tag,
notTag
} = c.req.valid('query') } = c.req.valid('query')
const projects = await db.query.projects.findMany({ const projects = await db.query.projects.findMany({
// List projects that are not private and have at least one published deployment // List projects that are not private and have at least one published deployment
// And optionally match a given tag
where: and( where: and(
eq(schema.projects.private, false), 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: { with: {
lastPublishedDeployment: true, lastPublishedDeployment: true,

Wyświetl plik

@ -37,6 +37,11 @@ export const projectIdentifierQuerySchema = z.object({
projectIdentifier: projectIdentifierSchema projectIdentifier: projectIdentifierSchema
}) })
export const filterPublicProjectSchema = z.object({
tag: z.string().optional(),
notTag: z.string().optional()
})
export const populateProjectSchema = z.object({ export const populateProjectSchema = z.object({
populate: z populate: z
.union([projectRelationsSchema, z.array(projectRelationsSchema)]) .union([projectRelationsSchema, z.array(projectRelationsSchema)])
@ -54,3 +59,9 @@ export const paginationAndPopulateProjectSchema = z.object({
...paginationSchema.shape, ...paginationSchema.shape,
...populateProjectSchema.shape ...populateProjectSchema.shape
}) })
export const listPublicProjectsQuerySchema = z.object({
...paginationSchema.shape,
...populateProjectSchema.shape,
...filterPublicProjectSchema.shape
})

Wyświetl plik

@ -73,7 +73,7 @@ export const deployments = pgTable(
name: projectName().notNull(), name: projectName().notNull(),
description: text().default('').notNull(), description: text().default('').notNull(),
readme: text().default('').notNull(), readme: text(), // URL to uploaded markdown document
iconUrl: text(), iconUrl: text(),
sourceUrl: text(), sourceUrl: text(),
homepageUrl: text(), homepageUrl: text(),

Wyświetl plik

@ -85,6 +85,9 @@ export const projects = pgTable(
// visible on the marketplace. // visible on the marketplace.
private: boolean().default(true).notNull(), 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 // TODO: allow for multiple aliases like vercel
// alias: text(), // alias: text(),
@ -172,6 +175,7 @@ export const projects = pgTable(
index('project_teamId_idx').on(table.teamId), index('project_teamId_idx').on(table.teamId),
// index('project_alias_idx').on(table.alias), // index('project_alias_idx').on(table.alias),
index('project_private_idx').on(table.private), index('project_private_idx').on(table.private),
index('project_tags_idx').on(table.tags),
index('project_lastPublishedDeploymentId_idx').on( index('project_lastPublishedDeploymentId_idx').on(
table.lastPublishedDeploymentId table.lastPublishedDeploymentId
), ),
@ -215,6 +219,7 @@ export const projectSelectBaseSchema = createSelectSchema(projects, {
identifier: projectIdentifierSchema, identifier: projectIdentifierSchema,
name: agenticProjectConfigSchema.shape.name, name: agenticProjectConfigSchema.shape.name,
slug: agenticProjectConfigSchema.shape.slug, slug: agenticProjectConfigSchema.shape.slug,
tags: z.array(z.string()).optional(),
lastPublishedDeploymentId: deploymentIdSchema.optional(), lastPublishedDeploymentId: deploymentIdSchema.optional(),
lastDeploymentId: deploymentIdSchema.optional(), lastDeploymentId: deploymentIdSchema.optional(),

Wyświetl plik

@ -8,11 +8,29 @@ import { LoadingIndicator } from '@/components/loading-indicator'
import { PageContainer } from '@/components/page-container' import { PageContainer } from '@/components/page-container'
import { PublicProject } from '@/components/public-project' import { PublicProject } from '@/components/public-project'
import { SupplySideCTA } from '@/components/supply-side-cta' import { SupplySideCTA } from '@/components/supply-side-cta'
import { useInfiniteQuery } from '@/lib/query-client' import { useInfiniteQuery, useQuery } from '@/lib/query-client'
export function MarketplaceIndex() { export function MarketplaceIndex() {
const ctx = useAgentic() const ctx = useAgentic()
const limit = 10 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 { const {
data, data,
isLoading, isLoading,
@ -21,13 +39,14 @@ export function MarketplaceIndex() {
fetchNextPage, fetchNextPage,
isFetchingNextPage isFetchingNextPage
} = useInfiniteQuery({ } = useInfiniteQuery({
queryKey: ['projects'], queryKey: ['public-projects'],
queryFn: ({ pageParam = 0 }) => queryFn: ({ pageParam = 0 }) =>
ctx!.api ctx!.api
.listPublicProjects({ .listPublicProjects({
populate: ['lastPublishedDeployment'], populate: ['lastPublishedDeployment'],
offset: pageParam, offset: pageParam,
limit limit,
notTag: 'featured'
}) })
.then(async (projects) => { .then(async (projects) => {
return { return {
@ -55,7 +74,7 @@ export function MarketplaceIndex() {
return ( return (
<PageContainer> <PageContainer>
<section> <section className='flex flex-col gap-8'>
<h1 <h1
className='text-center text-balance leading-snug md:leading-none className='text-center text-balance leading-snug md:leading-none
text-4xl font-extrabold' text-4xl font-extrabold'
@ -63,17 +82,38 @@ export function MarketplaceIndex() {
Marketplace Marketplace
</h1> </h1>
{!ctx || isLoading ? ( <div className='flex flex-col gap-16'>
<LoadingIndicator /> <div className=''>
) : ( <h2 className='text-xl font-semibold mb-4'>Featured</h2>
<div className='mt-8'>
<h2 className='text-xl font-semibold mb-4'>Public Projects</h2> {isFeaturedProjectsError ? (
<p>Error fetching featured projects</p>
) : isFeaturedProjectsLoading ? (
<LoadingIndicator />
) : !featuredProjects?.length ? (
<p>
No projects found. This is likely an issue on Agentic's side.
Please refresh or contact support.
</p>
) : (
<div className='grid grid-cols grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3'>
{featuredProjects.map((project) => (
<PublicProject key={project.id} project={project} />
))}
</div>
)}
</div>
<div className=''>
<h2 className='text-xl font-semibold mb-4'>General</h2>
{isError ? ( {isError ? (
<p>Error fetching projects</p> <p>Error fetching projects</p>
) : isLoading ? (
<LoadingIndicator />
) : !projects.length ? ( ) : !projects.length ? (
<p> <p>
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. Please refresh or contact support.
</p> </p>
) : ( ) : (
@ -90,7 +130,7 @@ export function MarketplaceIndex() {
</div> </div>
)} )}
</div> </div>
)} </div>
</section> </section>
{/* CTA section */} {/* CTA section */}
@ -99,7 +139,7 @@ export function MarketplaceIndex() {
Your API Paid MCP, Instantly Your API Paid MCP, Instantly
</h2> </h2>
<h5 className='text-center max-w-2xl'> <h5 className='text-center max-w-2xl bg-background/50 rounded-xl'>
Run one command to turn any MCP server or OpenAPI service into a paid 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 MCP product. With built-in support for every major LLM SDK and MCP
client. client.

Wyświetl plik

@ -36,11 +36,9 @@ export function PublicProject({ project }: { project: Project }) {
</div> </div>
<div className='flex-1 flex flex-col gap-3 justify-between'> <div className='flex-1 flex flex-col gap-3 justify-between'>
{deployment.description && ( <p className='text-sm text-gray-700 line-clamp-4'>
<p className='text-sm text-gray-700 line-clamp-4'> {deployment.description}
{deployment.description} </p>
</p>
)}
{project.lastPublishedDeployment && ( {project.lastPublishedDeployment && (
<div className='text-xs text-gray-500 flex gap-3 items-center justify-between'> <div className='text-xs text-gray-500 flex gap-3 items-center justify-between'>

Wyświetl plik

@ -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`. */ /** @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; slug?: string;
private: boolean; private: boolean;
tags?: string[];
/** @description User id (e.g. "user_tz4a98xxat96iws9zmbrgj3a") */ /** @description User id (e.g. "user_tz4a98xxat96iws9zmbrgj3a") */
userId: string; userId: string;
/** @description Team id (e.g. "team_tz4a98xxat96iws9zmbrgj3a") */ /** @description Team id (e.g. "team_tz4a98xxat96iws9zmbrgj3a") */
@ -1364,6 +1365,8 @@ export interface operations {
sort?: "asc" | "desc"; sort?: "asc" | "desc";
sortBy?: "createdAt" | "updatedAt"; sortBy?: "createdAt" | "updatedAt";
populate?: ("user" | "team" | "lastPublishedDeployment" | "lastDeployment") | ("user" | "team" | "lastPublishedDeployment" | "lastDeployment")[]; populate?: ("user" | "team" | "lastPublishedDeployment" | "lastDeployment") | ("user" | "team" | "lastPublishedDeployment" | "lastDeployment")[];
tag?: string;
notTag?: string;
}; };
header?: never; header?: never;
path?: never; path?: never;

12
todo.md
Wyświetl plik

@ -18,13 +18,12 @@
- docs: add notes about constraints on mcp origin servers (static tools) - docs: add notes about constraints on mcp origin servers (static tools)
- **api keys should go beyond 1:1 consumers** - **api keys should go beyond 1:1 consumers**
- **currently not obvious how to get api key** - **currently not obvious how to get api key**
- marketplace public project page - marketplace public project detail page
- mcp inspector
- **add support to example-usage for api keys**
- add last published date somewhere
- add breadcrumb nav: marketplace > @agentic > search - 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 - break out into a few subcomponents; some can be server components
- mcp inspector
- improve private project page - improve private project page
- link to public page if published - link to public page if published
- list deployment versions - list deployment versions
@ -34,9 +33,6 @@
- marketplace index page - marketplace index page
- add disclaimer about public beta - add disclaimer about public beta
- add search / sorting - 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) - replace render for api and/or add turbo for caching (too slow to deploy)
- create slack + notifications - create slack + notifications
- consider changing homepage hero CTA to include publishing - consider changing homepage hero CTA to include publishing