Encrypted, ephemeral, private memos on atproto
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 126 lines 3.4 kB view raw
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2import { getLogger } from "@logtape/logtape"; 3import { z } from "zod"; 4import type { 5 Consumer, 6 ConsumerOptions, 7 DecryptedMemo, 8} from "@cistern/consumer"; 9import { createConsumer } from "@cistern/consumer"; 10import { serializeKey } from "@cistern/crypto"; 11import { getStoredKeypair, storeKeypair } from "./kv.ts"; 12 13export async function createServer(options: ConsumerOptions) { 14 const logger = getLogger(["cistern", "mcp"]); 15 16 if (!options.keypair) { 17 const storedKeypair = await getStoredKeypair(options.handle); 18 if (storedKeypair) { 19 logger.info("using stored keypair for {handle}", { 20 handle: options.handle, 21 }); 22 options.keypair = storedKeypair; 23 } 24 } else { 25 logger.info("using keypair from environment variables"); 26 } 27 28 const consumer = await createConsumer(options); 29 30 if (!consumer.keypair) { 31 logger.info("no keypair found; generating new keypair for {handle}", { 32 handle: options.handle, 33 }); 34 35 const keypair = await consumer.generateKeyPair(); 36 37 logger.info("generated new keypair with public key URI: {publicKey}", { 38 publicKey: keypair.publicKey, 39 }); 40 41 await storeKeypair(options.handle, { 42 privateKey: serializeKey(keypair.privateKey), 43 publicKey: keypair.publicKey, 44 }); 45 } 46 47 return _createServerWithConsumer(consumer); 48} 49 50function _createServerWithConsumer(consumer: Consumer) { 51 const logger = getLogger("cistern-mcp"); 52 const server = new McpServer({ 53 name: "cistern-mcp", 54 version: "1.0.0", 55 }); 56 57 let iterator: 58 | AsyncGenerator<DecryptedMemo, void, "stop" | undefined> 59 | undefined; 60 61 server.registerTool( 62 "next_memo", 63 { 64 title: "Next memo", 65 description: "Retrieve the next outstanding memo", 66 outputSchema: { key: z.string(), tid: z.string(), text: z.string() }, 67 }, 68 async () => { 69 if (!iterator) { 70 logger.trace("creating iterator"); 71 iterator ??= consumer.listMemos(); 72 } 73 74 const res = await iterator.next(); 75 76 if (res.done) { 77 logger.trace("iterator done; cleaning up"); 78 iterator = undefined; 79 } 80 81 return { 82 content: [{ 83 type: "text", 84 text: res.value?.text 85 ? `key: ${res.value.key}, text: ${res.value.text}` 86 : "no memos remaining", 87 }], 88 structuredContent: { 89 key: res.value?.key ?? "", 90 tid: res.value?.tid ?? "", 91 text: res.value?.text ?? "no memos remaining", 92 }, 93 }; 94 }, 95 ); 96 97 server.registerTool( 98 "delete_memo", 99 { 100 title: "Delete memo", 101 description: 102 "Delete a memo by record key, after it has been handled as instructed by the user", 103 inputSchema: { key: z.string() }, 104 outputSchema: { success: z.boolean() }, 105 }, 106 async ({ key }) => { 107 try { 108 await consumer.deleteMemo(key); 109 110 return { 111 content: [{ type: "text", text: "delete successful" }], 112 structuredContent: { success: true }, 113 }; 114 } catch (error) { 115 logger.error("failed to delete memo: {error}", { error }); 116 117 return { 118 content: [{ type: "text", text: "delete unsuccessful" }], 119 structuredContent: { success: false }, 120 }; 121 } 122 }, 123 ); 124 125 return server; 126}