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/eslint.config.js b/eslint.config.js index a927e56..b8ceb7d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -45,5 +45,12 @@ export default ts.config( svelteConfig } } + }, + { + // button.svelte is a generic pass-through component — callers are responsible for resolve() + files: ['src/lib/components/ui/button/button.svelte'], + rules: { + 'svelte/no-navigation-without-resolve': 'off' + } } ); 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} +
+

Common Schemas

+ +
+ {/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/ServerNoteCard.svelte b/src/lib/components/ServerNoteCard.svelte index f395fd3..bd5bc74 100644 --- a/src/lib/components/ServerNoteCard.svelte +++ b/src/lib/components/ServerNoteCard.svelte @@ -146,10 +146,11 @@ {segment.value} {:else if segment.type === 'link'} + {segment.value} diff --git a/src/lib/components/ServerReviewsSection.svelte b/src/lib/components/ServerReviewsSection.svelte index afd5acf..939b733 100644 --- a/src/lib/components/ServerReviewsSection.svelte +++ b/src/lib/components/ServerReviewsSection.svelte @@ -55,7 +55,7 @@ const canPublishReview = $derived(!!composerContent.trim() && !isPublishing); $effect(() => { - refreshCount; + void refreshCount; isLoading = true; isRefreshing = true; const loaderSubscription = createServerReviewsLoader(pubkey, relayHints).subscribe(); diff --git a/src/lib/components/ServerTagCloud.svelte b/src/lib/components/ServerTagCloud.svelte new file mode 100644 index 0000000..e3b1f97 --- /dev/null +++ b/src/lib/components/ServerTagCloud.svelte @@ -0,0 +1,17 @@ + + +
+ +
diff --git a/src/lib/components/ToolCallForm.svelte b/src/lib/components/ToolCallForm.svelte index c017722..104e356 100644 --- a/src/lib/components/ToolCallForm.svelte +++ b/src/lib/components/ToolCallForm.svelte @@ -1,4 +1,5 @@ {#if href} +
{ + 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; + }) + ); +} + +/** 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/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 951839d..4656007 100644 --- a/src/lib/services/loaders.svelte.ts +++ b/src/lib/services/loaders.svelte.ts @@ -6,7 +6,14 @@ 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, + createTagServersFilter +} from '$lib/constants'; import { relayStore } from '../stores/relay-store.svelte'; import { PROMPTS_LIST_KIND, @@ -139,3 +146,32 @@ export const createNoteEventLoader = (id: string, relays?: string[]) => { relays: relays && relays.length > 0 ? mergeRelaySets(relays, commonRelays) : commonRelays }); }; + +export const createCommonSchemaAnnouncementsLoader = (relays?: string[]) => { + const selectedRelays = relays || relayStore.selectedRelays; + const loader = createTimelineLoader(relayPool, selectedRelays, commonSchemasFilter, { + eventStore + }); + return loader(); +}; + +export const createSchemaProviderLoader = (hash: string, relays?: string[]) => { + const selectedRelays = relays || relayStore.selectedRelays; + const loader = createTimelineLoader( + relayPool, + selectedRelays, + createSchemaProvidersFilter(hash), + { + eventStore + } + ); + return loader(); +}; + +export const createTagServersLoader = (tag: string, relays?: string[]) => { + const selectedRelays = relays || relayStore.selectedRelays; + const loader = createTimelineLoader(relayPool, selectedRelays, createTagServersFilter(tag), { + eventStore + }); + return loader(); +}; 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/lib/utils/cep15.ts b/src/lib/utils/cep15.ts new file mode 100644 index 0000000..2244b28 --- /dev/null +++ b/src/lib/utils/cep15.ts @@ -0,0 +1,55 @@ +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) { + 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; +} + +/** 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/s/[pubkey]/+page.svelte b/src/routes/s/[pubkey]/+page.svelte index 1d2b3f6..22cd88e 100644 --- a/src/routes/s/[pubkey]/+page.svelte +++ b/src/routes/s/[pubkey]/+page.svelte @@ -164,12 +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 toolsAnnouncementTags = $derived($toolsQuery?.data?.tags ?? []); let activeTab = $state('about'); @@ -283,10 +283,12 @@ {$serverQuery.data.server.name} {#if $serverQuery.data.server.website} + + {$serverQuery.data.server.website} @@ -424,7 +426,7 @@ {tool} serverPubkey={connectionIdentifier} {connectionState} - {announcementTags} + announcementTags={toolsAnnouncementTags} /> {/each} {:else} @@ -479,7 +481,7 @@ {resource} serverPubkey={connectionIdentifier} {connectionState} - {announcementTags} + announcementTags={toolsAnnouncementTags} /> {/each} {/if} @@ -569,7 +571,7 @@ {prompt} {connectionState} serverPubkey={connectionIdentifier} - {announcementTags} + announcementTags={toolsAnnouncementTags} /> {/each} {:else} @@ -636,6 +638,7 @@ @@ -653,10 +656,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 f9e81fa..8449b40 100644 --- a/src/routes/servers/+page.svelte +++ b/src/routes/servers/+page.svelte @@ -6,9 +6,12 @@ import { useServerAnnouncement, useServerAnnouncements } from '$lib/queries/serverQueries'; 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'; + import CatalogBrowseSection from '$lib/components/CatalogBrowseSection.svelte'; import { decodeServerIdentifier, encodeServerIdentity, @@ -16,9 +19,29 @@ } from '$lib/utils'; const serverAnnouncements = eventStore.model(ServerAnnouncementsModel); + const allSchemas = eventStore.model(CatalogSchemasModel); + + // All unique categories and schemas across the network for the discovery strip + const allCategories = $derived.by(() => { + const seen: string[] = []; + for (const schema of $allSchemas || []) { + for (const cat of schema.categories) { + if (!seen.includes(cat)) seen.push(cat); + } + } + return seen.sort(); + }); + const schemaBadges = $derived( + ($allSchemas || []).map((schema) => ({ ...schema, providerCount: schema.providers.length })) + ); const serverAnnouncementsQuery = useServerAnnouncements(); + $effect(() => { + const sub = createCommonSchemaAnnouncementsLoader().subscribe(); + return () => sub.unsubscribe(); + }); + let loading = $state($serverAnnouncementsQuery.isFetching); let searchTerm = $state(''); const resolvedSearchIdentifierQuery = $derived.by(() => { @@ -84,6 +107,17 @@

+ + {#if allCategories.length > 0 || schemaBadges.length > 0} +
+ +
+ {/if} +
diff --git a/src/routes/servers/i/[hash]/+page.svelte b/src/routes/servers/i/[hash]/+page.svelte new file mode 100644 index 0000000..c28ddb5 --- /dev/null +++ b/src/routes/servers/i/[hash]/+page.svelte @@ -0,0 +1,155 @@ + + + + +
+
+ + + + +
+

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

+

+ MCP servers implementing this common tool schema. +

+ +
+ +
+ + +
+ + {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..58fa69c --- /dev/null +++ b/src/routes/servers/t/[cat]/+page.svelte @@ -0,0 +1,139 @@ + + + + +
+
+ + + + +
+

+ #{cat} +

+

+ MCP servers in the {cat} category. +

+
+ +
+
+ +
+ + +

+ 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)}