nostr-bray 1.24.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/format.d.ts CHANGED
@@ -54,3 +54,4 @@ export declare function formatWikiList(topics: any[]): string;
54
54
  export declare function formatScheduleResult(result: any): string;
55
55
  export declare function formatScheduledQueue(entries: any[]): string;
56
56
  export declare function formatDispatchReplyResult(result: any): string;
57
+ export declare function formatHandlers(handlers: any[]): string;
package/dist/format.js CHANGED
@@ -467,3 +467,24 @@ export function formatDispatchReplyResult(result) {
467
467
  const del = result.deleted ? ' (original message deleted)' : '';
468
468
  return `Sent ${result.messageType}${del}`;
469
469
  }
470
+ // --- Handler formatters ---
471
+ export function formatHandlers(handlers) {
472
+ if (handlers.length === 0)
473
+ return 'No MCP handlers found.';
474
+ return handlers.map(h => {
475
+ const lines = [`${h.name} (${h.pubkey?.slice(0, 12)}...)`];
476
+ if (h.about)
477
+ lines.push(` ${h.about}`);
478
+ if (h.kinds?.length > 0)
479
+ lines.push(` Kinds: ${h.kinds.join(', ')}`);
480
+ if (h.transports?.length > 0) {
481
+ const transports = h.transports.map((t) => `${t.transport}: ${t.endpoint}`).join(', ');
482
+ lines.push(` Transports: ${transports}`);
483
+ }
484
+ if (h.dTag)
485
+ lines.push(` d-tag: ${h.dTag}`);
486
+ if (h.picture)
487
+ lines.push(` Picture: ${h.picture}`);
488
+ return lines.join('\n');
489
+ }).join('\n\n');
490
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * NIP-XX Machine Application Handler tools.
3
+ *
4
+ * Publishes kind 31990 events with MCP transport tags, and discovers
5
+ * MCP-capable handlers on Nostr.
6
+ */
7
+ import type { Event as NostrEvent } from 'nostr-tools';
8
+ import type { SigningContext } from '../signing-context.js';
9
+ import type { RelayPool } from '../relay-pool.js';
10
+ import type { PublishResult } from '../types.js';
11
+ export interface HandlerTransport {
12
+ endpoint: string;
13
+ transport: 'stdio' | 'http';
14
+ }
15
+ export interface HandlerCard {
16
+ pubkey: string;
17
+ name: string;
18
+ about: string;
19
+ kinds: string[];
20
+ transports: HandlerTransport[];
21
+ dTag: string;
22
+ picture?: string;
23
+ }
24
+ export interface HandlerPublishResult {
25
+ event: NostrEvent;
26
+ publish: PublishResult;
27
+ }
28
+ /**
29
+ * Publish a kind 31990 event advertising MCP handler capabilities.
30
+ *
31
+ * At least one of stdioCommand or httpUrl must be provided.
32
+ */
33
+ export declare function handleHandlerPublish(ctx: SigningContext, pool: RelayPool, args: {
34
+ name: string;
35
+ about: string;
36
+ kinds: string[];
37
+ stdioCommand?: string;
38
+ httpUrl?: string;
39
+ picture?: string;
40
+ dTag?: string;
41
+ }): Promise<HandlerPublishResult>;
42
+ /**
43
+ * Discover MCP-capable handlers on Nostr via kind 31990 events.
44
+ *
45
+ * Filters results to only include events that contain at least one mcp tag.
46
+ */
47
+ export declare function handleHandlerDiscover(pool: RelayPool, npub: string, args: {
48
+ kind?: string;
49
+ limit?: number;
50
+ }): Promise<HandlerCard[]>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * NIP-XX Machine Application Handler tools.
3
+ *
4
+ * Publishes kind 31990 events with MCP transport tags, and discovers
5
+ * MCP-capable handlers on Nostr.
6
+ */
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+ /** Slugify a name into a URL-safe d-tag identifier */
11
+ function slugify(name) {
12
+ return name
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9]+/g, '-')
15
+ .replace(/^-+|-+$/g, '');
16
+ }
17
+ /** Parse a kind 31990 event into a HandlerCard, returning null if it has no mcp tags */
18
+ function parseHandlerEvent(event) {
19
+ try {
20
+ const mcpTags = event.tags.filter(t => t[0] === 'mcp');
21
+ if (mcpTags.length === 0)
22
+ return null;
23
+ const content = JSON.parse(event.content);
24
+ const dTag = event.tags.find(t => t[0] === 'd');
25
+ const kTags = event.tags.filter(t => t[0] === 'k');
26
+ const transports = mcpTags.map(t => ({
27
+ endpoint: t[1] ?? '',
28
+ transport: (t[2] ?? 'stdio'),
29
+ }));
30
+ return {
31
+ pubkey: event.pubkey,
32
+ name: content.name ?? '',
33
+ about: content.about ?? '',
34
+ picture: content.picture,
35
+ kinds: kTags.map(t => t[1]),
36
+ transports,
37
+ dTag: dTag?.[1] ?? '',
38
+ };
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // handleHandlerPublish
46
+ // ---------------------------------------------------------------------------
47
+ /**
48
+ * Publish a kind 31990 event advertising MCP handler capabilities.
49
+ *
50
+ * At least one of stdioCommand or httpUrl must be provided.
51
+ */
52
+ export async function handleHandlerPublish(ctx, pool, args) {
53
+ if (!args.stdioCommand && !args.httpUrl) {
54
+ throw new Error('At least one of stdioCommand or httpUrl must be provided.');
55
+ }
56
+ const dTag = args.dTag ?? slugify(args.name);
57
+ const now = Math.floor(Date.now() / 1000);
58
+ const contentObj = {
59
+ name: args.name,
60
+ about: args.about,
61
+ };
62
+ if (args.picture)
63
+ contentObj.picture = args.picture;
64
+ const content = JSON.stringify(contentObj);
65
+ const kindList = args.kinds.join(', ');
66
+ const tags = [
67
+ ['d', dTag],
68
+ ...args.kinds.map(k => ['k', k]),
69
+ ['alt', `MCP handler for kind ${kindList}`],
70
+ ];
71
+ if (args.stdioCommand)
72
+ tags.push(['mcp', args.stdioCommand, 'stdio']);
73
+ if (args.httpUrl)
74
+ tags.push(['mcp', args.httpUrl, 'http']);
75
+ const sign = ctx.getSigningFunction();
76
+ const event = await sign({
77
+ kind: 31990,
78
+ created_at: now,
79
+ tags,
80
+ content,
81
+ });
82
+ const publish = await pool.publish(ctx.activeNpub, event);
83
+ return { event, publish };
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // handleHandlerDiscover
87
+ // ---------------------------------------------------------------------------
88
+ /**
89
+ * Discover MCP-capable handlers on Nostr via kind 31990 events.
90
+ *
91
+ * Filters results to only include events that contain at least one mcp tag.
92
+ */
93
+ export async function handleHandlerDiscover(pool, npub, args) {
94
+ const limit = args.limit ?? 20;
95
+ const filter = {
96
+ kinds: [31990],
97
+ limit,
98
+ };
99
+ if (args.kind) {
100
+ filter['#k'] = [args.kind];
101
+ }
102
+ const events = await pool.query(npub, filter);
103
+ const cards = [];
104
+ for (const event of events) {
105
+ const card = parseHandlerEvent(event);
106
+ if (card)
107
+ cards.push(card);
108
+ }
109
+ return cards;
110
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ToolDeps } from '../identity/tools.js';
3
+ export declare function registerHandlerTools(server: McpServer, deps: ToolDeps): void;
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ import { toolResponse } from '../tool-response.js';
3
+ import * as fmt from '../format.js';
4
+ import { handleHandlerPublish, handleHandlerDiscover } from './handlers.js';
5
+ export function registerHandlerTools(server, deps) {
6
+ // --- handler-publish ---
7
+ server.registerTool('handler-publish', {
8
+ description: 'Publish a NIP-XX Machine Application Handler card (kind 31990) advertising MCP transport endpoints. Lets other agents and tools discover MCP servers on Nostr by the event kinds they support. Provide at least one of stdio_command or http_url.',
9
+ inputSchema: {
10
+ name: z.string().describe('Handler name (e.g. "nostr-bray", "402-mcp")'),
11
+ about: z.string().describe('What this handler does'),
12
+ kinds: z.array(z.string()).describe('Event kinds this handler supports (e.g. ["1", "31402"])'),
13
+ stdio_command: z.string().optional().describe('stdio transport command (e.g. "npx nostr-bray")'),
14
+ http_url: z.string().optional().describe('HTTP transport URL (e.g. "https://mcp.example.com/sse")'),
15
+ picture: z.string().optional().describe('Icon URL'),
16
+ d_tag: z.string().optional().describe('Custom d-tag identifier (defaults to slugified name)'),
17
+ output: z.enum(['json', 'human']).default('json').describe('Response format'),
18
+ },
19
+ annotations: { readOnlyHint: false },
20
+ }, async ({ name, about, kinds, stdio_command, http_url, picture, d_tag, output }) => {
21
+ const result = await handleHandlerPublish(deps.ctx, deps.pool, {
22
+ name,
23
+ about,
24
+ kinds,
25
+ stdioCommand: stdio_command,
26
+ httpUrl: http_url,
27
+ picture,
28
+ dTag: d_tag,
29
+ });
30
+ return toolResponse(result, output, fmt.formatPublish);
31
+ });
32
+ // --- handler-discover ---
33
+ server.registerTool('handler-discover', {
34
+ description: 'Discover MCP-capable handlers on Nostr. Searches for kind 31990 events that include mcp transport tags. Optionally filter by supported event kind. Returns handler names, descriptions, supported kinds, and transport endpoints.',
35
+ inputSchema: {
36
+ kind: z.string().optional().describe('Filter by supported event kind (e.g. "1", "31402")'),
37
+ limit: z.number().optional().describe('Maximum number of results (default: 20)'),
38
+ output: z.enum(['json', 'human']).default('human').describe('Response format'),
39
+ },
40
+ annotations: { readOnlyHint: true },
41
+ }, async ({ kind, limit, output }) => {
42
+ const result = await handleHandlerDiscover(deps.pool, deps.ctx.activeNpub, {
43
+ kind,
44
+ limit,
45
+ });
46
+ return toolResponse(result, output, fmt.formatHandlers);
47
+ });
48
+ }
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { TrustContext } from './trust-context.js';
20
20
  import { registerSignetTools } from './signet/tools.js';
21
21
  import { registerVaultTools } from './vault/tools.js';
22
22
  import { registerDispatchTools } from './dispatch/tools.js';
23
+ import { registerHandlerTools } from './handler/tools.js';
23
24
  import { ActionCatalog, createCatalogProxy } from './catalog.js';
24
25
  const config = await loadConfig();
25
26
  const pool = new RelayPool({
@@ -112,6 +113,7 @@ registerModerationTools(proxy, deps);
112
113
  registerSignetTools(proxy, deps);
113
114
  registerVaultTools(proxy, deps);
114
115
  registerDispatchTools(proxy, { ...deps, dispatchIdentitiesPath: config.dispatchIdentities });
116
+ registerHandlerTools(proxy, deps);
115
117
  // Add search-actions and execute-action meta-tools to the real server
116
118
  catalog.registerMetaTools(server);
117
119
  console.error(`nostr-bray: ${PROMOTED.size} promoted tools + ${catalog.size} cataloged (${PROMOTED.size + catalog.size + 2} total)`);
package/dist/serve.js CHANGED
@@ -8,6 +8,7 @@ import { WebSocketServer, WebSocket } from 'ws';
8
8
  import { createServer } from 'node:http';
9
9
  import { readFileSync } from 'node:fs';
10
10
  import { verifyEvent } from 'nostr-tools/pure';
11
+ let subCounter = 0;
11
12
  /** Check if an event matches a single filter */
12
13
  function matchFilter(filter, event) {
13
14
  if (filter.ids && !filter.ids.includes(event.id))
@@ -117,7 +118,7 @@ export function startRelay(opts = {}) {
117
118
  const filters = msg.slice(2);
118
119
  // Register subscription
119
120
  const sub = { id: subId, filters, ws };
120
- const key = `${subId}-${Date.now()}`;
121
+ const key = `${subId}-${++subCounter}`;
121
122
  subscriptions.set(key, sub);
122
123
  clientSubs.add(key);
123
124
  // Send matching stored events
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-bray",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Trust-aware Nostr for AI and humans. Three dimensions of trust -- verification, proximity, and access -- woven into every interaction.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",