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