From 56ca8309bafe10b38b3154ac30d730352ed3df05 Mon Sep 17 00:00:00 2001 From: Abhay Gupta Date: Mon, 18 May 2026 20:42:16 +0530 Subject: [PATCH 1/8] feat(cep15): implement data layer for common tool schemas - Add utility functions to extract CEP-15 tags (i, k, t) - Define nostr filters for common schema discovery - Implement Applesauce loaders and models to deduplicate and parse kind:11317 events - Add TanStack query hooks for catalog data fetching Signed-off-by: Siddhi Gupta Signed-off-by: Abhay Gupta --- src/lib/constants.ts | 15 ++++++ src/lib/models/catalogSchemas.ts | 84 +++++++++++++++++++++++++++++ src/lib/queries/catalogQueries.ts | 26 +++++++++ src/lib/queries/catalogQueryKeys.ts | 4 ++ src/lib/services/loaders.svelte.ts | 29 +++++++++- src/lib/utils/cep15.ts | 41 ++++++++++++++ 6 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 src/lib/models/catalogSchemas.ts create mode 100644 src/lib/queries/catalogQueries.ts create mode 100644 src/lib/queries/catalogQueryKeys.ts create mode 100644 src/lib/utils/cep15.ts diff --git a/src/lib/constants.ts b/src/lib/constants.ts index aef94fe..ff17040 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,6 +1,9 @@ import { SERVER_ANNOUNCEMENT_KIND } from '@contextvm/sdk'; import type { Filter } from 'nostr-tools'; import { LongFormArticle, ShortTextNote } from 'nostr-tools/kinds'; +import { TOOLS_LIST_KIND } from '@contextvm/sdk'; + +export const COMMON_SCHEMA_NAMESPACE = 'io.contextvm/common-schema'; export const CONTEXTVM_PUBKEY = '6b3780ef2972e73d370b84a3e51e7aa9ae34bf412938dcfbd9c5f63b221416c8'; @@ -20,3 +23,15 @@ export function createServerNotesFilter(pubkey: string): Filter { limit: 5 }; } + +export const commonSchemasFilter: Filter = { + kinds: [TOOLS_LIST_KIND], + '#k': [COMMON_SCHEMA_NAMESPACE] +}; + +export function createSchemaProvidersFilter(hash: string): Filter { + return { + kinds: [TOOLS_LIST_KIND], + '#i': [hash] + }; +} diff --git a/src/lib/models/catalogSchemas.ts b/src/lib/models/catalogSchemas.ts new file mode 100644 index 0000000..66c2d4d --- /dev/null +++ b/src/lib/models/catalogSchemas.ts @@ -0,0 +1,84 @@ +import { map } from 'rxjs/operators'; +import type { Model } from 'applesauce-core'; +import type { Event } from 'nostr-tools'; +import { extractCategories, extractCommonSchemas } from '$lib/utils/cep15'; +import { TOOLS_LIST_KIND } from '@contextvm/sdk'; + +export interface CatalogSchemaGroup { + hash: string; + name: string; + categories: string[]; + providers: string[]; // array of pubkeys +} + +export function CatalogSchemasModel(): Model { + return (events) => + events.timeline({ kinds: [TOOLS_LIST_KIND] }).pipe( + map((events: Event[]) => { + const latestEventsByPubkey = new Map(); + + for (const event of events) { + const existing = latestEventsByPubkey.get(event.pubkey); + if (!existing || event.created_at > existing.created_at) { + latestEventsByPubkey.set(event.pubkey, event); + } + } + + const groups = new Map(); + + for (const event of latestEventsByPubkey.values()) { + const schemas = extractCommonSchemas(event); + const categories = extractCategories(event); + + for (const schema of schemas) { + let group = groups.get(schema.hash); + if (!group) { + group = { + hash: schema.hash, + name: schema.name, + categories: [], + providers: [] + }; + groups.set(schema.hash, group); + } + + if (!group.providers.includes(event.pubkey)) { + group.providers.push(event.pubkey); + } + + for (const cat of categories) { + if (!group.categories.includes(cat)) { + group.categories.push(cat); + } + } + } + } + + return Array.from(groups.values()); + }) + ); +} + +export function SchemaProvidersModel(hash: string): Model { + return (events) => + events.timeline({ kinds: [TOOLS_LIST_KIND] }).pipe( + map((events: Event[]) => { + const latestEventsByPubkey = new Map(); + for (const event of events) { + const existing = latestEventsByPubkey.get(event.pubkey); + if (!existing || event.created_at > existing.created_at) { + latestEventsByPubkey.set(event.pubkey, event); + } + } + + const providers: string[] = []; + for (const event of latestEventsByPubkey.values()) { + const schemas = extractCommonSchemas(event); + if (schemas.some((s) => s.hash === hash)) { + providers.push(event.pubkey); + } + } + return providers; + }) + ); +} diff --git a/src/lib/queries/catalogQueries.ts b/src/lib/queries/catalogQueries.ts new file mode 100644 index 0000000..086042a --- /dev/null +++ b/src/lib/queries/catalogQueries.ts @@ -0,0 +1,26 @@ +import { createQuery } from '@tanstack/svelte-query'; +import { catalogKeys } from './catalogQueryKeys'; +import { + createCommonSchemaAnnouncementsLoader, + createSchemaProviderLoader +} from '$lib/services/loaders.svelte'; +import { lastValueFrom } from 'rxjs'; +import type { Event } from 'nostr-tools'; + +export function useCatalogSchemas() { + return createQuery({ + queryKey: catalogKeys.all, + queryFn: async () => { + return await lastValueFrom(createCommonSchemaAnnouncementsLoader()); + } + }); +} + +export function useSchemaProviders(hash: string) { + return createQuery({ + queryKey: catalogKeys.providers(hash), + queryFn: async () => { + return await lastValueFrom(createSchemaProviderLoader(hash)); + } + }); +} diff --git a/src/lib/queries/catalogQueryKeys.ts b/src/lib/queries/catalogQueryKeys.ts new file mode 100644 index 0000000..2fc1e0d --- /dev/null +++ b/src/lib/queries/catalogQueryKeys.ts @@ -0,0 +1,4 @@ +export const catalogKeys = { + all: ['catalog'] as const, + providers: (hash: string) => [...catalogKeys.all, 'providers', hash] as const +} as const; diff --git a/src/lib/services/loaders.svelte.ts b/src/lib/services/loaders.svelte.ts index 951839d..72549ce 100644 --- a/src/lib/services/loaders.svelte.ts +++ b/src/lib/services/loaders.svelte.ts @@ -6,7 +6,13 @@ import { import { COMMENT_KIND } from 'applesauce-common/helpers'; import { commonRelays, defaultRelays, relayPool } from './relay-pool'; import { eventStore } from './eventStore'; -import { articlesFilter, createServerNotesFilter, serverAnnouncementsFilter } from '$lib/constants'; +import { + articlesFilter, + createServerNotesFilter, + serverAnnouncementsFilter, + commonSchemasFilter, + createSchemaProvidersFilter +} from '$lib/constants'; import { relayStore } from '../stores/relay-store.svelte'; import { PROMPTS_LIST_KIND, @@ -139,3 +145,24 @@ export const createNoteEventLoader = (id: string, relays?: string[]) => { relays: relays && relays.length > 0 ? mergeRelaySets(relays, commonRelays) : commonRelays }); }; + +export const createCommonSchemaAnnouncementsLoader = (relays?: string[]) => { + const selectedRelays = mergeRelaySets(relays || relayStore.selectedRelays, commonRelays); + const loader = createTimelineLoader(relayPool, selectedRelays, commonSchemasFilter, { + eventStore + }); + return loader(); +}; + +export const createSchemaProviderLoader = (hash: string, relays?: string[]) => { + const selectedRelays = mergeRelaySets(relays || relayStore.selectedRelays, commonRelays); + const loader = createTimelineLoader( + relayPool, + selectedRelays, + createSchemaProvidersFilter(hash), + { + eventStore + } + ); + return loader(); +}; diff --git a/src/lib/utils/cep15.ts b/src/lib/utils/cep15.ts new file mode 100644 index 0000000..837422b --- /dev/null +++ b/src/lib/utils/cep15.ts @@ -0,0 +1,41 @@ +import type { Event } from 'nostr-tools'; +import type { Tool } from '@modelcontextprotocol/sdk/types.js'; + +export interface CEP15SchemaInfo { + hash: string; + name: string; +} + +export function extractCommonSchemas(event: Event): CEP15SchemaInfo[] { + const schemas: CEP15SchemaInfo[] = []; + for (const tag of event.tags) { + if (tag[0] === 'i' && tag.length >= 3) { + schemas.push({ hash: tag[1], name: tag[2] }); + } + } + return schemas; +} + +export function extractCategories(event: Event): string[] { + const categories = new Set(); + for (const tag of event.tags) { + if (tag[0] === 't' && tag.length >= 2) { + categories.add(tag[1]); + } + } + return Array.from(categories); +} + +export function getToolSchemaHash(tool: Tool): string | undefined { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const meta = (tool as any)._meta; + if ( + meta && + typeof meta === 'object' && + 'schemaHash' in meta && + typeof meta.schemaHash === 'string' + ) { + return meta.schemaHash; + } + return undefined; +} From 18136acc7c24262f3eff718b6f1de47e11d9adc7 Mon Sep 17 00:00:00 2001 From: Abhay Gupta Date: Wed, 20 May 2026 15:52:41 +0530 Subject: [PATCH 2/8] feat(cep15): implement phase 2 catalog UI and routing - Add 'Browse Common Schemas' CTA button on servers page - Create root /catalog route to list all discovered common schemas - Create /catalog/[hash] detail route to list providers for specific schemas - Implement search filtering for schema names and categories Signed-off-by: Abhay Gupta --- src/routes/catalog/+page.svelte | 133 +++++++++++++++++++++++++ src/routes/catalog/[hash]/+page.svelte | 108 ++++++++++++++++++++ src/routes/servers/+page.svelte | 9 ++ 3 files changed, 250 insertions(+) create mode 100644 src/routes/catalog/+page.svelte create mode 100644 src/routes/catalog/[hash]/+page.svelte diff --git a/src/routes/catalog/+page.svelte b/src/routes/catalog/+page.svelte new file mode 100644 index 0000000..856333f --- /dev/null +++ b/src/routes/catalog/+page.svelte @@ -0,0 +1,133 @@ + + + +
+
+ +
+

Browse Common Schemas

+

+ Discover standardized CEP-15 tool schemas implemented across the network. Choose a schema to + see which servers provide it. +

+
+ + +
+
+ +
+ + {#if $query.isFetching && !$schemas?.length} +
+ {#each Array(6) as _, i} +
+ {/each} +
+ {:else if filteredSchemas.length > 0} +
+

+ Available Schemas + {#if searchTerm} + + ({filteredSchemas.length} results) + + {/if} +

+
+ + {:else} +
+

No schemas found matching your search.

+
+ {/if} +
+
+
diff --git a/src/routes/catalog/[hash]/+page.svelte b/src/routes/catalog/[hash]/+page.svelte new file mode 100644 index 0000000..8d54b3c --- /dev/null +++ b/src/routes/catalog/[hash]/+page.svelte @@ -0,0 +1,108 @@ + + + + +
+
+ +
+

+ {currentSchema?.name || 'Schema Providers'} +

+

+ The following MCP servers implement this common tool schema. +

+ {#if currentSchema?.categories?.length} +
+ {#each currentSchema.categories as cat} + #{cat} + {/each} +
+ {/if} +
+ + {hash} +
+
+ +
+ {#if loading && !providers.length} +
+ {#each Array(3) as _} +
+ {/each} +
+ {:else if providers.length > 0} +
+ {#each providers as server (server.id)} +
+ +
+ {/each} +
+ {:else} +
+

No known public servers provide this schema yet.

+
+ {/if} +
+
+
diff --git a/src/routes/servers/+page.svelte b/src/routes/servers/+page.svelte index f9e81fa..2c5098b 100644 --- a/src/routes/servers/+page.svelte +++ b/src/routes/servers/+page.svelte @@ -82,6 +82,15 @@ Discover and connect with Model Context Protocol servers running on the Nostr network. No domains, no OAuth, no port forwarding—just cryptographic keys and relays.

+ +
+ +
From 4ac9c0b72f9bd16f38ec29a755ffb3b5d2804775 Mon Sep 17 00:00:00 2001 From: Abhay Gupta Date: Wed, 20 May 2026 16:07:11 +0530 Subject: [PATCH 3/8] feat(cep15): implement phase 3 cross-navigation - Parse common schema NIP-73 tags directly from announcement payload - Inject schema hash badges inline in tool form headers - Provide seamless routing back to the catalog provider list Signed-off-by: Siddhi Gupta --- src/lib/components/ToolCallForm.svelte | 15 +++++++++++++++ src/routes/catalog/[hash]/+page.ts | 1 + 2 files changed, 16 insertions(+) create mode 100644 src/routes/catalog/[hash]/+page.ts diff --git a/src/lib/components/ToolCallForm.svelte b/src/lib/components/ToolCallForm.svelte index c017722..9d1c43a 100644 --- a/src/lib/components/ToolCallForm.svelte +++ b/src/lib/components/ToolCallForm.svelte @@ -47,6 +47,10 @@ return runtimeCapTags.length > 0 ? runtimeCapTags : parseCapTagsFromTags(announcementTags); }); const toolCap = $derived(findCapTagForTool(capTags, tool.name)); + + const commonSchemaTag = $derived(announcementTags?.find(t => t[0] === 'i' && t[2] === tool.name)); + const schemaHash = $derived(commonSchemaTag?.[1]); + $effect(() => { // Auto-collapse when we have a final result. if (showResult && formResult) paymentOpen = false; @@ -117,6 +121,17 @@ Paid · {formatCapTagPrice(toolCap)} {/if} + {#if schemaHash} + e.stopPropagation()} + > + + {schemaHash.substring(0, 8)} + + {/if} {#if tool.description}

{tool.description}

diff --git a/src/routes/catalog/[hash]/+page.ts b/src/routes/catalog/[hash]/+page.ts new file mode 100644 index 0000000..d43d0cd --- /dev/null +++ b/src/routes/catalog/[hash]/+page.ts @@ -0,0 +1 @@ +export const prerender = false; From d0532fc80d015a53fe3f360750775e3a58e2dcc1 Mon Sep 17 00:00:00 2001 From: Abhay Gupta Date: Wed, 20 May 2026 20:17:06 +0530 Subject: [PATCH 4/8] feat(cep15): complete implementation of common schemas catalog and cross-navigation Signed-off-by: Abhay Gupta --- src/lib/components/ToolCallForm.svelte | 26 ++++++++++++++++++++++--- src/lib/models/catalogSchemas.ts | 4 ++-- src/lib/queries/catalogQueries.ts | 1 - src/routes/catalog/+page.svelte | 7 ++++--- src/routes/catalog/[hash]/+page.svelte | 14 +++++-------- src/routes/servers/+page.svelte | 27 +++++++++++++++++++++++--- 6 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/lib/components/ToolCallForm.svelte b/src/lib/components/ToolCallForm.svelte index 9d1c43a..49d30ec 100644 --- a/src/lib/components/ToolCallForm.svelte +++ b/src/lib/components/ToolCallForm.svelte @@ -1,4 +1,5 @@ + +{#if hasContent} +
+

+ Browse Catalog +

+ + {#if categories.length > 0} +
+

Categories

+
+ {#each categories as cat (cat)} + + #{cat} + + {/each} +
+
+ {/if} + + {#if schemas.length > 0} +
+

Common Schemas

+
+ {#each schemas as schema (schema.hash)} + + {formatSchemaLabel(schema.name, schema.hash)} + + {/each} +
+
+ {/if} +
+{/if} diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte index 16f1a3c..8c0280d 100644 --- a/src/lib/components/ui/button/button.svelte +++ b/src/lib/components/ui/button/button.svelte @@ -54,6 +54,7 @@ {#if href} + { }) ); } + +/** A model that returns pubkeys of servers that have a specific t-tag category */ +export function TagServersModel(tag: string): Model { + return (events) => + events.timeline({ kinds: [TOOLS_LIST_KIND] }).pipe( + map((events: Event[]) => { + const latestEventsByPubkey = new Map(); + for (const event of events) { + const existing = latestEventsByPubkey.get(event.pubkey); + if (!existing || event.created_at > existing.created_at) { + latestEventsByPubkey.set(event.pubkey, event); + } + } + + const providers: string[] = []; + for (const event of latestEventsByPubkey.values()) { + const categories = extractCategories(event); + if (categories.includes(tag)) { + providers.push(event.pubkey); + } + } + return providers; + }) + ); +} diff --git a/src/lib/services/loaders.svelte.ts b/src/lib/services/loaders.svelte.ts index 72549ce..1edf2e8 100644 --- a/src/lib/services/loaders.svelte.ts +++ b/src/lib/services/loaders.svelte.ts @@ -11,7 +11,8 @@ import { createServerNotesFilter, serverAnnouncementsFilter, commonSchemasFilter, - createSchemaProvidersFilter + createSchemaProvidersFilter, + createTagServersFilter } from '$lib/constants'; import { relayStore } from '../stores/relay-store.svelte'; import { @@ -166,3 +167,11 @@ export const createSchemaProviderLoader = (hash: string, relays?: string[]) => { ); return loader(); }; + +export const createTagServersLoader = (tag: string, relays?: string[]) => { + const selectedRelays = mergeRelaySets(relays || relayStore.selectedRelays, commonRelays); + const loader = createTimelineLoader(relayPool, selectedRelays, createTagServersFilter(tag), { + eventStore + }); + return loader(); +}; diff --git a/src/lib/utils/cep15.ts b/src/lib/utils/cep15.ts index 837422b..565b775 100644 --- a/src/lib/utils/cep15.ts +++ b/src/lib/utils/cep15.ts @@ -39,3 +39,9 @@ export function getToolSchemaHash(tool: Tool): string | undefined { } return undefined; } + +/** Formats a schema for display: 'tool_name:ha...sh' */ +export function formatSchemaLabel(name: string, hash: string): string { + const shortHash = hash.length > 12 ? hash.substring(0, 8) + '...' : hash; + return `${name}:${shortHash}`; +} diff --git a/src/routes/catalog/+page.svelte b/src/routes/catalog/+page.svelte index 3b03368..da1c8f3 100644 --- a/src/routes/catalog/+page.svelte +++ b/src/routes/catalog/+page.svelte @@ -1,134 +1,11 @@ - -
- -
+

Redirecting to Servers...

diff --git a/src/routes/catalog/[hash]/+page.svelte b/src/routes/catalog/[hash]/+page.svelte index 899e20d..10db05f 100644 --- a/src/routes/catalog/[hash]/+page.svelte +++ b/src/routes/catalog/[hash]/+page.svelte @@ -1,104 +1,14 @@ - - -
-
- -
-

- {currentSchema?.name || 'Schema Providers'} -

-

- The following MCP servers implement this common tool schema. -

- {#if currentSchema?.categories?.length} -
- {#each currentSchema.categories as cat (cat)} - #{cat} - {/each} -
- {/if} -
- - {hash} -
-
- -
- {#if loading && !providers.length} -
- {#each Array(3) as _, idx (idx)} -
- {/each} -
- {:else if providers.length > 0} -
- {#each providers as server (server.id)} -
- -
- {/each} -
- {:else} -
-

No known public servers provide this schema yet.

-
- {/if} -
-
-
+

Redirecting...

diff --git a/src/routes/s/[pubkey]/+page.svelte b/src/routes/s/[pubkey]/+page.svelte index 1d2b3f6..6b4c45e 100644 --- a/src/routes/s/[pubkey]/+page.svelte +++ b/src/routes/s/[pubkey]/+page.svelte @@ -48,6 +48,7 @@ import LoadingSpinner from '$lib/components/ui/LoadingSpinner.svelte'; import { createServerNotesFilter } from '$lib/constants'; import { npubEncode } from 'nostr-tools/nip19'; + import ServerTagCloud from '$lib/components/ServerTagCloud.svelte'; const requestedIdentifier = page.params.pubkey ?? ''; const resolvedIdentifierQuery = createQuery({ @@ -283,10 +284,12 @@ {$serverQuery.data.server.name} {#if $serverQuery.data.server.website} + + {$serverQuery.data.server.website} @@ -621,6 +624,9 @@ + + + @@ -653,10 +659,12 @@ {#each $notes as note (note.id)} {/each} + + Open profile diff --git a/src/routes/servers/+page.svelte b/src/routes/servers/+page.svelte index 43a3bf4..fce47e4 100644 --- a/src/routes/servers/+page.svelte +++ b/src/routes/servers/+page.svelte @@ -1,14 +1,17 @@ + + + +
+
+ + + + +
+

+ {currentSchema?.name || hash.substring(0, 16) + '…'} +

+

+ MCP servers implementing this common tool schema. +

+ + + {#if currentSchema?.categories?.length} +
+ {#each currentSchema.categories as cat (cat)} + + #{cat} + + {/each} +
+ {/if} + + +
+ + {hash} +
+
+ + +
+ {#if loading && !providers.length} +
+ {#each Array(3) as _, idx (idx)} +
+ {/each} +
+ {:else if providers.length > 0} +
+ {#each providers as server (server.id)} +
+ +
+ {/each} +
+ {:else} +
+

No known public servers implement this schema yet.

+ + Browse all servers + +
+ {/if} +
+
+
diff --git a/src/routes/servers/i/[hash]/+page.ts b/src/routes/servers/i/[hash]/+page.ts new file mode 100644 index 0000000..d43d0cd --- /dev/null +++ b/src/routes/servers/i/[hash]/+page.ts @@ -0,0 +1 @@ +export const prerender = false; diff --git a/src/routes/servers/t/[cat]/+page.svelte b/src/routes/servers/t/[cat]/+page.svelte new file mode 100644 index 0000000..28b9414 --- /dev/null +++ b/src/routes/servers/t/[cat]/+page.svelte @@ -0,0 +1,144 @@ + + + + +
+
+ + + + +
+

+ #{cat} +

+

+ MCP servers in the {cat} category. +

+
+ +
+ + {#if relatedSchemas.length > 0} +
+

Common Schemas in this category

+
+ {#each relatedSchemas as schema (schema.hash)} + + {schema.name} + :{schema.hash.substring(0, 8)}... + + {schema.providers.length} + + + {/each} +
+
+ {/if} + + +

+ Servers + {#if !loading && providers.length > 0} + ({providers.length}) + {/if} +

+ + {#if loading && !providers.length} +
+ {#each Array(3) as _, idx (idx)} +
+ {/each} +
+ {:else if providers.length > 0} +
+ {#each providers as server (server.id)} +
+ +
+ {/each} +
+ {:else} +
+

No servers found in the {cat} category yet.

+ + Browse all servers + +
+ {/if} +
+
+
diff --git a/src/routes/servers/t/[cat]/+page.ts b/src/routes/servers/t/[cat]/+page.ts new file mode 100644 index 0000000..d43d0cd --- /dev/null +++ b/src/routes/servers/t/[cat]/+page.ts @@ -0,0 +1 @@ +export const prerender = false; diff --git a/src/routes/slides/+page.svelte b/src/routes/slides/+page.svelte index 7ca6230..04401d6 100644 --- a/src/routes/slides/+page.svelte +++ b/src/routes/slides/+page.svelte @@ -85,9 +85,10 @@
- {#each slideDecks as deck} + {#each slideDecks as deck (deck.href)}
From df824293ad8572c96e23e69e188670d9c82ab374 Mon Sep 17 00:00:00 2001 From: Abhay Gupta Date: Mon, 25 May 2026 14:58:54 +0530 Subject: [PATCH 6/8] refactor(cep15): enforce strict parsing constraints, wire reactive discovery, and clean up legacy catalog routes Signed-off-by: Abhay Gupta --- src/lib/components/ToolCallForm.svelte | 2 +- src/lib/queries/catalogQueries.ts | 25 ---------------- src/lib/queries/catalogQueryKeys.ts | 4 --- src/lib/utils/cep15.ts | 8 ++++++ src/routes/catalog/+page.svelte | 11 -------- src/routes/catalog/[hash]/+page.svelte | 14 --------- src/routes/catalog/[hash]/+page.ts | 1 - src/routes/s/[pubkey]/+page.svelte | 10 ++++--- src/routes/servers/+page.svelte | 36 ++++-------------------- src/routes/servers/i/[hash]/+page.svelte | 18 +++++------- src/routes/servers/t/[cat]/+page.svelte | 16 ++++------- 11 files changed, 34 insertions(+), 111 deletions(-) delete mode 100644 src/lib/queries/catalogQueries.ts delete mode 100644 src/lib/queries/catalogQueryKeys.ts delete mode 100644 src/routes/catalog/+page.svelte delete mode 100644 src/routes/catalog/[hash]/+page.svelte delete mode 100644 src/routes/catalog/[hash]/+page.ts diff --git a/src/lib/components/ToolCallForm.svelte b/src/lib/components/ToolCallForm.svelte index 49d30ec..104e356 100644 --- a/src/lib/components/ToolCallForm.svelte +++ b/src/lib/components/ToolCallForm.svelte @@ -126,7 +126,7 @@ {/if} {#if schemaHash} e.stopPropagation()} diff --git a/src/lib/queries/catalogQueries.ts b/src/lib/queries/catalogQueries.ts deleted file mode 100644 index 6a29cff..0000000 --- a/src/lib/queries/catalogQueries.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createQuery } from '@tanstack/svelte-query'; -import { catalogKeys } from './catalogQueryKeys'; -import { - createCommonSchemaAnnouncementsLoader, - createSchemaProviderLoader -} from '$lib/services/loaders.svelte'; -import { lastValueFrom } from 'rxjs'; - -export function useCatalogSchemas() { - return createQuery({ - queryKey: catalogKeys.all, - queryFn: async () => { - return await lastValueFrom(createCommonSchemaAnnouncementsLoader()); - } - }); -} - -export function useSchemaProviders(hash: string) { - return createQuery({ - queryKey: catalogKeys.providers(hash), - queryFn: async () => { - return await lastValueFrom(createSchemaProviderLoader(hash)); - } - }); -} diff --git a/src/lib/queries/catalogQueryKeys.ts b/src/lib/queries/catalogQueryKeys.ts deleted file mode 100644 index 2fc1e0d..0000000 --- a/src/lib/queries/catalogQueryKeys.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const catalogKeys = { - all: ['catalog'] as const, - providers: (hash: string) => [...catalogKeys.all, 'providers', hash] as const -} as const; diff --git a/src/lib/utils/cep15.ts b/src/lib/utils/cep15.ts index 565b775..2244b28 100644 --- a/src/lib/utils/cep15.ts +++ b/src/lib/utils/cep15.ts @@ -1,12 +1,20 @@ import type { Event } from 'nostr-tools'; import type { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { COMMON_SCHEMA_NAMESPACE } from '$lib/constants'; + export interface CEP15SchemaInfo { hash: string; name: string; } export function extractCommonSchemas(event: Event): CEP15SchemaInfo[] { + const hasCommonSchemaNamespace = event.tags.some( + (tag) => tag[0] === 'k' && tag[1] === COMMON_SCHEMA_NAMESPACE + ); + + if (!hasCommonSchemaNamespace) return []; + const schemas: CEP15SchemaInfo[] = []; for (const tag of event.tags) { if (tag[0] === 'i' && tag.length >= 3) { diff --git a/src/routes/catalog/+page.svelte b/src/routes/catalog/+page.svelte deleted file mode 100644 index da1c8f3..0000000 --- a/src/routes/catalog/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -

Redirecting to Servers...

diff --git a/src/routes/catalog/[hash]/+page.svelte b/src/routes/catalog/[hash]/+page.svelte deleted file mode 100644 index 10db05f..0000000 --- a/src/routes/catalog/[hash]/+page.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -

Redirecting...

diff --git a/src/routes/catalog/[hash]/+page.ts b/src/routes/catalog/[hash]/+page.ts deleted file mode 100644 index d43d0cd..0000000 --- a/src/routes/catalog/[hash]/+page.ts +++ /dev/null @@ -1 +0,0 @@ -export const prerender = false; diff --git a/src/routes/s/[pubkey]/+page.svelte b/src/routes/s/[pubkey]/+page.svelte index 6b4c45e..e921da3 100644 --- a/src/routes/s/[pubkey]/+page.svelte +++ b/src/routes/s/[pubkey]/+page.svelte @@ -171,6 +171,8 @@ prompts: $promptsQuery?.data || null }); const announcementTags = $derived($serverQuery?.data?.server?.tags ?? []); + const toolsListEvent = $derived(mcpClientService.getServerToolsListEvent(pubkey)); + const toolsListTags = $derived(toolsListEvent?.tags ?? []); let activeTab = $state('about'); @@ -427,7 +429,7 @@ {tool} serverPubkey={connectionIdentifier} {connectionState} - {announcementTags} + announcementTags={toolsListTags} /> {/each} {:else} @@ -482,7 +484,7 @@ {resource} serverPubkey={connectionIdentifier} {connectionState} - {announcementTags} + announcementTags={toolsListTags} /> {/each} {/if} @@ -572,7 +574,7 @@ {prompt} {connectionState} serverPubkey={connectionIdentifier} - {announcementTags} + announcementTags={toolsListTags} /> {/each} {:else} @@ -625,7 +627,7 @@
- + diff --git a/src/routes/servers/+page.svelte b/src/routes/servers/+page.svelte index fce47e4..39f0aca 100644 --- a/src/routes/servers/+page.svelte +++ b/src/routes/servers/+page.svelte @@ -8,6 +8,7 @@ import { eventStore } from '$lib/services/eventStore'; import { ServerAnnouncementsModel } from '$lib/models/serverAnnouncements'; import { CatalogSchemasModel } from '$lib/models/catalogSchemas'; + import { createCommonSchemaAnnouncementsLoader } from '$lib/services/loaders.svelte'; import Seo from '$lib/components/SEO.svelte'; import { Input } from '$lib/components/ui/input'; import { Button } from '$lib/components/ui/button'; @@ -34,6 +35,11 @@ const serverAnnouncementsQuery = useServerAnnouncements(); + $effect(() => { + const sub = createCommonSchemaAnnouncementsLoader().subscribe(); + return () => sub.unsubscribe(); + }); + let loading = $state($serverAnnouncementsQuery.isFetching); let searchTerm = $state(''); const resolvedSearchIdentifierQuery = $derived.by(() => { @@ -97,36 +103,6 @@ Discover and connect with Model Context Protocol servers running on the Nostr network. No domains, no OAuth, no port forwarding—just cryptographic keys and relays.

- -
- -
diff --git a/src/routes/servers/i/[hash]/+page.svelte b/src/routes/servers/i/[hash]/+page.svelte index 0946eb1..5ea4a0c 100644 --- a/src/routes/servers/i/[hash]/+page.svelte +++ b/src/routes/servers/i/[hash]/+page.svelte @@ -6,9 +6,9 @@ import { ServerAnnouncementsModel } from '$lib/models/serverAnnouncements'; import { createSchemaProviderLoader, - createServerAnnouncementsLoader, createCommonSchemaAnnouncementsLoader } from '$lib/services/loaders.svelte'; + import { useServerAnnouncements } from '$lib/queries/serverQueries'; import ServerCard from '$lib/components/ServerCard.svelte'; import LoadingCard from '$lib/components/LoadingCard.svelte'; import Seo from '$lib/components/SEO.svelte'; @@ -34,29 +34,25 @@ return $serverAnnouncements.filter((s) => providerPubkeys.includes(s.pubkey)); }); + // Use the existing query to track loading state for server announcements + const serverAnnouncementsQuery = useServerAnnouncements(); let loading = $state(true); $effect(() => { - loading = true; - const subs = [ createCommonSchemaAnnouncementsLoader().subscribe(), - createSchemaProviderLoader(hash).subscribe(), - createServerAnnouncementsLoader().subscribe() + createSchemaProviderLoader(hash).subscribe() ]; - const timer = setTimeout(() => { - loading = false; - }, 5000); - return () => { subs.forEach((s) => s.unsubscribe()); - clearTimeout(timer); }; }); + // Drop loading state once we have our initial server query resolved + // (or if we already have matching providers) $effect(() => { - if ($serverAnnouncements !== undefined && $providerPubkeysStore !== undefined) { + if (!$serverAnnouncementsQuery.isFetching || providers.length > 0) { loading = false; } }); diff --git a/src/routes/servers/t/[cat]/+page.svelte b/src/routes/servers/t/[cat]/+page.svelte index 28b9414..c66ce16 100644 --- a/src/routes/servers/t/[cat]/+page.svelte +++ b/src/routes/servers/t/[cat]/+page.svelte @@ -6,9 +6,9 @@ import { ServerAnnouncementsModel } from '$lib/models/serverAnnouncements'; import { createTagServersLoader, - createServerAnnouncementsLoader, createCommonSchemaAnnouncementsLoader } from '$lib/services/loaders.svelte'; + import { useServerAnnouncements } from '$lib/queries/serverQueries'; import ServerCard from '$lib/components/ServerCard.svelte'; import LoadingCard from '$lib/components/LoadingCard.svelte'; import Seo from '$lib/components/SEO.svelte'; @@ -30,29 +30,25 @@ return $serverAnnouncements.filter((s) => providerPubkeys.includes(s.pubkey)); }); + // Use the existing query to track loading state for server announcements + const serverAnnouncementsQuery = useServerAnnouncements(); let loading = $state(true); $effect(() => { - loading = true; - const subs = [ createTagServersLoader(cat).subscribe(), - createServerAnnouncementsLoader().subscribe(), createCommonSchemaAnnouncementsLoader().subscribe() ]; - const timer = setTimeout(() => { - loading = false; - }, 5000); - return () => { subs.forEach((s) => s.unsubscribe()); - clearTimeout(timer); }; }); + // Drop loading state once we have our initial server query resolved + // (or if we already have matching providers) $effect(() => { - if ($serverAnnouncements !== undefined && $providerPubkeysStore !== undefined) { + if (!$serverAnnouncementsQuery.isFetching || providers.length > 0) { loading = false; } }); From 27b7124fb18f46f85cc3e009346fedb1c3bf7552 Mon Sep 17 00:00:00 2001 From: ContextVM Date: Tue, 26 May 2026 11:10:28 +0200 Subject: [PATCH 7/8] feat(cep15): collaborate on catalog browse, server queries, and schema updates --- bun.lock | 24 +++++-- package.json | 2 +- .../components/CatalogBrowseSection.svelte | 67 +++++++++++++++++++ .../components/ServerInformationCard.svelte | 16 ++++- src/lib/components/ServerTagCloud.svelte | 48 ++----------- src/lib/models/catalogSchemas.ts | 1 - src/lib/queries/serverQueries.ts | 30 +++++++-- src/lib/services/loaders.svelte.ts | 6 +- src/lib/stores/relay-store.svelte.ts | 2 +- src/routes/s/[pubkey]/+page.svelte | 17 ++--- src/routes/servers/+page.svelte | 52 +++----------- src/routes/servers/i/[hash]/+page.svelte | 30 +++++---- src/routes/servers/t/[cat]/+page.svelte | 42 ++++++------ 13 files changed, 187 insertions(+), 150 deletions(-) create mode 100644 src/lib/components/CatalogBrowseSection.svelte diff --git a/bun.lock b/bun.lock index 90831c8..06cd105 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "contextvm-site", "dependencies": { - "@contextvm/sdk": "^0.8.0", + "@contextvm/sdk": "^0.11", "@sjsf/ajv8-validator": "^3.3.2", "@sjsf/form": "^3.3.2", "@sjsf/shadcn4-theme": "^3.3.2", @@ -97,7 +97,7 @@ "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], - "@contextvm/sdk": ["@contextvm/sdk@0.8.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", "@noble/hashes": "^2.0.1", "applesauce-relay": "^5.1.0", "nostr-tools": "~2.18.2", "pino": "^10.3.1", "rxjs": "^7.8.2", "ws": "^8.18.3", "zod": "^4.3.5" }, "peerDependencies": { "typescript": "^5.9.2" } }, "sha512-R4g3nz2aE3PRUxYjU8mDgqpw+YuP00wATQT6xF44nGfXi0XNrvWUxIQnuCt7yT+68VybW9K+EVW/NQQgT+GB8A=="], + "@contextvm/sdk": ["@contextvm/sdk@0.11.11", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "@noble/hashes": "^2.2.0", "applesauce-relay": "^5.2.0", "canonicalize": "^2.1.0", "nostr-tools": "~2.18.2", "pino": "^10.3.1", "rxjs": "^7.8.2", "ws": "^8.20.0", "zod": "^4.4.3" }, "peerDependencies": { "typescript": "^5.9.3" } }, "sha512-wpfxLMYUSvCm3vOgGkbS3oSQlFzgz7TjIszgZv9e3d9Noi97eOKlo3+O4WTdBJtl3xK4w4bTiIZUDp76a764jg=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], @@ -207,13 +207,13 @@ "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], "@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="], - "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], "@noble/secp256k1": ["@noble/secp256k1@1.7.2", "", {}, "sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ=="], @@ -429,6 +429,8 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "canonicalize": ["canonicalize@2.1.0", "", { "bin": { "canonicalize": "bin/canonicalize.js" } }, "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], @@ -973,7 +975,7 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], @@ -991,6 +993,8 @@ "@contextvm/sdk/nostr-tools": ["nostr-tools@2.18.2", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-lUCJQd9YZG3kEvxV5Zgm7qUkBpaeuvFrtqBz4TJLAxHzUn2pE7nmZZRDQmNzp5neEw20tQS3jR16o7XzzF8ncg=="], + "@contextvm/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/config-helpers/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], @@ -1009,8 +1013,16 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@noble/curves/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + + "@scure/bip32/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@scure/bip32/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], + "@scure/bip39/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@scure/bip39/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1061,6 +1073,8 @@ "mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], + "nostr-tools/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "nostr-tools/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], diff --git a/package.json b/package.json index 2da256c..d6935c4 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "vite": "^7.3.1" }, "dependencies": { - "@contextvm/sdk": "^0.8.0", + "@contextvm/sdk": "^0.11", "@sjsf/ajv8-validator": "^3.3.2", "@sjsf/form": "^3.3.2", "@sjsf/shadcn4-theme": "^3.3.2", diff --git a/src/lib/components/CatalogBrowseSection.svelte b/src/lib/components/CatalogBrowseSection.svelte new file mode 100644 index 0000000..0681230 --- /dev/null +++ b/src/lib/components/CatalogBrowseSection.svelte @@ -0,0 +1,67 @@ + + +{#if hasContent} +
+

+ {title} +

+ + {#if categories.length > 0} +
0 ? 'mb-4' : ''}> +

Categories

+
+ {#each categories as category (category)} + + #{category} + + {/each} +
+
+ {/if} + + {#if schemas.length > 0} + + {/if} +
+{/if} diff --git a/src/lib/components/ServerInformationCard.svelte b/src/lib/components/ServerInformationCard.svelte index b54268f..4f66e26 100644 --- a/src/lib/components/ServerInformationCard.svelte +++ b/src/lib/components/ServerInformationCard.svelte @@ -11,8 +11,17 @@ import { mcpClientService } from '$lib/services/mcpClient.svelte'; import CopyIcon from '@lucide/svelte/icons/copy'; import { parsePmiTagsFromEvent } from '$lib/services/payments/cep8-tags'; + import ServerTagCloud from '$lib/components/ServerTagCloud.svelte'; - let { server, identity }: { server: ServerAnnouncement; identity?: ServerIdentity } = $props(); + let { + server, + identity, + tags = [] + }: { + server: ServerAnnouncement; + identity?: ServerIdentity; + tags?: ServerAnnouncement['tags']; + } = $props(); let activeIdentityTab = $state<'npub' | 'hex' | 'nprofile'>('npub'); const publishedAt = $derived(formatUnixTimestamp(server.created_at, true)); @@ -147,6 +156,11 @@ {/if} +
+
+ +
+
diff --git a/src/lib/components/ServerTagCloud.svelte b/src/lib/components/ServerTagCloud.svelte index e4d7b7e..e3b1f97 100644 --- a/src/lib/components/ServerTagCloud.svelte +++ b/src/lib/components/ServerTagCloud.svelte @@ -1,7 +1,7 @@ -{#if hasContent} -
-

- Browse Catalog -

- - {#if categories.length > 0} -
-

Categories

-
- {#each categories as cat (cat)} - - #{cat} - - {/each} -
-
- {/if} - - {#if schemas.length > 0} -
-

Common Schemas

-
- {#each schemas as schema (schema.hash)} - - {formatSchemaLabel(schema.name, schema.hash)} - - {/each} -
-
- {/if} -
-{/if} +
+ +
diff --git a/src/lib/models/catalogSchemas.ts b/src/lib/models/catalogSchemas.ts index ef2dfb8..044b434 100644 --- a/src/lib/models/catalogSchemas.ts +++ b/src/lib/models/catalogSchemas.ts @@ -16,7 +16,6 @@ export function CatalogSchemasModel(): Model { events.timeline({ kinds: [TOOLS_LIST_KIND] }).pipe( map((events: Event[]) => { const latestEventsByPubkey = new Map(); - for (const event of events) { const existing = latestEventsByPubkey.get(event.pubkey); if (!existing || event.created_at > existing.created_at) { diff --git a/src/lib/queries/serverQueries.ts b/src/lib/queries/serverQueries.ts index d3aa462..eb0c2a2 100644 --- a/src/lib/queries/serverQueries.ts +++ b/src/lib/queries/serverQueries.ts @@ -23,6 +23,11 @@ interface ServerQueryResult { server: ServerAnnouncement | null; } +export interface ServerToolsQueryResult { + tools: Tool[] | null; + tags: NostrEvent['tags']; +} + interface ParsedRelayList { relays: string[]; hasPublishedRelayList: boolean; @@ -81,7 +86,7 @@ export function useServerIdentity(pubkey: string, explicitRelayHints: string[] = } export function useServerTools(pubkey: string, isPublic: boolean, relayHints: string[] = []) { - return createQuery({ + return createQuery({ queryKey: serverKeys.capabilities.tools(pubkey), queryFn: async () => { if (isPublic) { @@ -136,24 +141,35 @@ export function useServerPrompts(pubkey: string, isPublic: boolean, relayHints: async function fetchToolsFromAnnouncements( pubkey: string, relayHints: string[] = [] -): Promise { +): Promise { const event = await lastValueFrom( createToolsAnnouncementByPubkeyLoader(pubkey, getLookupRelays(relayHints)) ); - if (!event) return null; + if (!event) { + return { tools: null, tags: [] }; + } try { const content = JSON.parse(event.content); - return content.tools || []; + return { + tools: content.tools || [], + tags: event.tags + }; } catch (err) { console.error('Failed to parse tools announcement:', err); - return null; + return { + tools: null, + tags: event.tags + }; } } -async function fetchToolsFromMCP(pubkey: string): Promise { +async function fetchToolsFromMCP(pubkey: string): Promise { try { const result = await mcpClientService.listTools(pubkey); - return result.tools; + return { + tools: result.tools, + tags: [] + }; } catch (error) { console.error('Failed to fetch tools from MCP:', error); throw error; diff --git a/src/lib/services/loaders.svelte.ts b/src/lib/services/loaders.svelte.ts index 1edf2e8..4656007 100644 --- a/src/lib/services/loaders.svelte.ts +++ b/src/lib/services/loaders.svelte.ts @@ -148,7 +148,7 @@ export const createNoteEventLoader = (id: string, relays?: string[]) => { }; export const createCommonSchemaAnnouncementsLoader = (relays?: string[]) => { - const selectedRelays = mergeRelaySets(relays || relayStore.selectedRelays, commonRelays); + const selectedRelays = relays || relayStore.selectedRelays; const loader = createTimelineLoader(relayPool, selectedRelays, commonSchemasFilter, { eventStore }); @@ -156,7 +156,7 @@ export const createCommonSchemaAnnouncementsLoader = (relays?: string[]) => { }; export const createSchemaProviderLoader = (hash: string, relays?: string[]) => { - const selectedRelays = mergeRelaySets(relays || relayStore.selectedRelays, commonRelays); + const selectedRelays = relays || relayStore.selectedRelays; const loader = createTimelineLoader( relayPool, selectedRelays, @@ -169,7 +169,7 @@ export const createSchemaProviderLoader = (hash: string, relays?: string[]) => { }; export const createTagServersLoader = (tag: string, relays?: string[]) => { - const selectedRelays = mergeRelaySets(relays || relayStore.selectedRelays, commonRelays); + const selectedRelays = relays || relayStore.selectedRelays; const loader = createTimelineLoader(relayPool, selectedRelays, createTagServersFilter(tag), { eventStore }); diff --git a/src/lib/stores/relay-store.svelte.ts b/src/lib/stores/relay-store.svelte.ts index db90bcd..08bc872 100644 --- a/src/lib/stores/relay-store.svelte.ts +++ b/src/lib/stores/relay-store.svelte.ts @@ -3,7 +3,7 @@ import { defaultRelays, devRelay } from '../services/relay-pool'; // Reactive relay store using Svelte 5 $state export const relayStore = $state({ - selectedRelays: dev ? devRelay : defaultRelays, + selectedRelays: !dev ? devRelay : defaultRelays, relayChangeCallback: null as ((relays: string[]) => void) | null }); diff --git a/src/routes/s/[pubkey]/+page.svelte b/src/routes/s/[pubkey]/+page.svelte index e921da3..22cd88e 100644 --- a/src/routes/s/[pubkey]/+page.svelte +++ b/src/routes/s/[pubkey]/+page.svelte @@ -48,7 +48,6 @@ import LoadingSpinner from '$lib/components/ui/LoadingSpinner.svelte'; import { createServerNotesFilter } from '$lib/constants'; import { npubEncode } from 'nostr-tools/nip19'; - import ServerTagCloud from '$lib/components/ServerTagCloud.svelte'; const requestedIdentifier = page.params.pubkey ?? ''; const resolvedIdentifierQuery = createQuery({ @@ -165,14 +164,12 @@ // Server capabilities data const serverData = $derived({ - tools: $toolsQuery?.data || null, + tools: $toolsQuery?.data?.tools || null, resources: $resourcesQuery?.data || null, resourceTemplates: $resourceTemplatesQuery?.data || null, prompts: $promptsQuery?.data || null }); - const announcementTags = $derived($serverQuery?.data?.server?.tags ?? []); - const toolsListEvent = $derived(mcpClientService.getServerToolsListEvent(pubkey)); - const toolsListTags = $derived(toolsListEvent?.tags ?? []); + const toolsAnnouncementTags = $derived($toolsQuery?.data?.tags ?? []); let activeTab = $state('about'); @@ -429,7 +426,7 @@ {tool} serverPubkey={connectionIdentifier} {connectionState} - announcementTags={toolsListTags} + announcementTags={toolsAnnouncementTags} /> {/each} {:else} @@ -484,7 +481,7 @@ {resource} serverPubkey={connectionIdentifier} {connectionState} - announcementTags={toolsListTags} + announcementTags={toolsAnnouncementTags} /> {/each} {/if} @@ -574,7 +571,7 @@ {prompt} {connectionState} serverPubkey={connectionIdentifier} - announcementTags={toolsListTags} + announcementTags={toolsAnnouncementTags} /> {/each} {:else} @@ -626,9 +623,6 @@ - - - @@ -644,6 +638,7 @@ diff --git a/src/routes/servers/+page.svelte b/src/routes/servers/+page.svelte index 39f0aca..a30f8f0 100644 --- a/src/routes/servers/+page.svelte +++ b/src/routes/servers/+page.svelte @@ -12,7 +12,7 @@ import Seo from '$lib/components/SEO.svelte'; import { Input } from '$lib/components/ui/input'; import { Button } from '$lib/components/ui/button'; - import { formatSchemaLabel } from '$lib/utils/cep15'; + import CatalogBrowseSection from '$lib/components/CatalogBrowseSection.svelte'; import { decodeServerIdentifier, encodeServerIdentity, @@ -32,6 +32,9 @@ } return seen.sort(); }); + const schemaBadges = $derived( + ($allSchemas || []).map((schema) => ({ ...schema, providerCount: schema.providers.length })) + ); const serverAnnouncementsQuery = useServerAnnouncements(); @@ -106,48 +109,13 @@ - {#if allCategories.length > 0 || ($allSchemas || []).length > 0} + {#if allCategories.length > 0 || schemaBadges.length > 0}
- {#if allCategories.length > 0} -
-

- Browse by Category -

-
- {#each allCategories as cat (cat)} - - #{cat} - - {/each} -
-
- {/if} - - {#if ($allSchemas || []).length > 0} -
-

- Browse by Common Schema -

-
- {#each $allSchemas || [] as schema (schema.hash)} - - {formatSchemaLabel(schema.name, schema.hash)} - - {schema.providers.length} - - - {/each} -
-
- {/if} +
{/if} diff --git a/src/routes/servers/i/[hash]/+page.svelte b/src/routes/servers/i/[hash]/+page.svelte index 5ea4a0c..c28ddb5 100644 --- a/src/routes/servers/i/[hash]/+page.svelte +++ b/src/routes/servers/i/[hash]/+page.svelte @@ -13,6 +13,7 @@ import LoadingCard from '$lib/components/LoadingCard.svelte'; import Seo from '$lib/components/SEO.svelte'; import { formatSchemaLabel } from '$lib/utils/cep15'; + import CatalogBrowseSection from '$lib/components/CatalogBrowseSection.svelte'; const hash = $derived(page.params.hash as string); @@ -22,6 +23,15 @@ const schemaLabel = $derived( currentSchema ? formatSchemaLabel(currentSchema.name, hash) : `${hash.substring(0, 8)}...` ); + const relatedSchemas = $derived( + ($allSchemas || []) + .filter( + (schema) => + schema.hash !== hash && + schema.categories.some((category) => currentSchema?.categories.includes(category)) + ) + .map((schema) => ({ ...schema, providerCount: schema.providers.length })) + ); // Provider pubkeys for this schema hash const providerPubkeysStore = $derived(eventStore.model(SchemaProvidersModel, hash)); @@ -81,19 +91,13 @@ MCP servers implementing this common tool schema.

- - {#if currentSchema?.categories?.length} -
- {#each currentSchema.categories as cat (cat)} - - #{cat} - - {/each} -
- {/if} +
+ +
diff --git a/src/routes/servers/t/[cat]/+page.svelte b/src/routes/servers/t/[cat]/+page.svelte index c66ce16..3188575 100644 --- a/src/routes/servers/t/[cat]/+page.svelte +++ b/src/routes/servers/t/[cat]/+page.svelte @@ -12,6 +12,7 @@ import ServerCard from '$lib/components/ServerCard.svelte'; import LoadingCard from '$lib/components/LoadingCard.svelte'; import Seo from '$lib/components/SEO.svelte'; + import CatalogBrowseSection from '$lib/components/CatalogBrowseSection.svelte'; const cat = $derived(page.params.cat as string); @@ -23,6 +24,18 @@ // All schemas — for displaying related schemas in this category const allSchemas = eventStore.model(CatalogSchemasModel); const relatedSchemas = $derived(($allSchemas || []).filter((s) => s.categories.includes(cat))); + const allCategories = $derived.by(() => { + const seen = new Set(); + for (const schema of relatedSchemas) { + for (const category of schema.categories) { + seen.add(category); + } + } + return Array.from(seen).sort(); + }); + const schemaBadges = $derived( + relatedSchemas.map((schema) => ({ ...schema, providerCount: schema.providers.length })) + ); // Matching server announcements const providers = $derived.by(() => { @@ -79,28 +92,13 @@
- - {#if relatedSchemas.length > 0} -
-

Common Schemas in this category

-
- {#each relatedSchemas as schema (schema.hash)} - - {schema.name} - :{schema.hash.substring(0, 8)}... - - {schema.providers.length} - - - {/each} -
-
- {/if} +
+ +

From a6c07a9967f38e8c97425f2e78255a4ed7a3a420 Mon Sep 17 00:00:00 2001 From: Abhay Gupta Date: Tue, 26 May 2026 15:16:30 +0530 Subject: [PATCH 8/8] fix(cep15): resolve linting errors introduced in catalog refactor Signed-off-by: Abhay Gupta --- src/routes/servers/+page.svelte | 1 - src/routes/servers/t/[cat]/+page.svelte | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/servers/+page.svelte b/src/routes/servers/+page.svelte index a30f8f0..8449b40 100644 --- a/src/routes/servers/+page.svelte +++ b/src/routes/servers/+page.svelte @@ -1,6 +1,5 @@