Encrypted, ephemeral, private memos on atproto

feat(mcp): enable cors, move to hono

graham.systems cda61a39 6dd67986

verified
Changed files
+148 -81
packages
+5 -6
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@hono/hono@^4.10.5": "4.10.5", 4 5 "jsr:@logtape/logtape@^1.2.0": "1.2.0", 5 6 "jsr:@noble/ciphers@^2.0.1": "2.0.1", 6 7 "jsr:@noble/curves@2.0": "2.0.1", ··· 28 29 "npm:zod@^3.25.76": "3.25.76" 29 30 }, 30 31 "jsr": { 32 + "@hono/hono@4.10.5": { 33 + "integrity": "13dbf2a528feb8189ad13394b213f0cf5f83b0ba4b2fadd0549993426db9ad2d" 34 + }, 31 35 "@logtape/logtape@1.2.0": { 32 36 "integrity": "8e1d3af5c91966cc5689cfb17081a36bccfdff28ff6314769185661f5147e74d" 33 37 }, ··· 52 56 }, 53 57 "@puregarlic/randimal@1.1.1": { 54 58 "integrity": "4e1fa61982cf2f610e9ad851d0fd0ff7bc3bb7b7a3c6cccae59f5ae2e68a7e47" 55 - }, 56 - "@std/assert@1.0.14": { 57 - "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", 58 - "dependencies": [ 59 - "jsr:@std/internal@^1.0.10" 60 - ] 61 59 }, 62 60 "@std/assert@1.0.15": { 63 61 "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", ··· 1724 1722 }, 1725 1723 "packages/mcp": { 1726 1724 "dependencies": [ 1725 + "jsr:@hono/hono@^4.10.5", 1727 1726 "jsr:@logtape/logtape@^1.2.0", 1728 1727 "jsr:@std/cli@^1.0.23", 1729 1728 "npm:@modelcontextprotocol/sdk@^1.21.1",
+1
packages/mcp/deno.jsonc
··· 17 17 } 18 18 }, 19 19 "imports": { 20 + "hono": "jsr:@hono/hono@^4.10.5", 20 21 "@logtape/logtape": "jsr:@logtape/logtape@^1.2.0", 21 22 "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.21.1", 22 23 "@std/cli": "jsr:@std/cli@^1.0.23",
+124
packages/mcp/hono.ts
··· 1 + import { Hono } from "hono"; 2 + import { cors } from "hono/cors"; 3 + import { getLogger, withContext } from "@logtape/logtape"; 4 + import { toFetchResponse, toReqRes } from "fetch-to-node"; 5 + import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 6 + import { createServer } from "./server.ts"; 7 + 8 + export function createApp() { 9 + const app = new Hono(); 10 + const logger = getLogger(["cistern", "http"]); 11 + const sessions = new Map<string, StreamableHTTPServerTransport>(); 12 + 13 + app.use("*", async (c, next) => { 14 + const requestId = crypto.randomUUID(); 15 + const startTime = Date.now(); 16 + 17 + await withContext({ 18 + requestId, 19 + method: c.req.method, 20 + url: c.req.url, 21 + userAgent: c.req.header("User-Agent"), 22 + ipAddress: c.req.header("CF-Connecting-IP") || 23 + c.req.header("X-Forwarded-For"), 24 + }, async () => { 25 + logger.info("{method} request started", { 26 + method: c.req.method, 27 + url: c.req.url, 28 + requestId, 29 + }); 30 + 31 + await next(); 32 + 33 + const duration = Date.now() - startTime; 34 + 35 + logger.info("{status} request completed in {duration}ms", { 36 + status: c.res.status, 37 + duration, 38 + requestId, 39 + }); 40 + }); 41 + }); 42 + 43 + app.onError((err, c) => { 44 + logger.error("request error", { 45 + error: { 46 + name: err.name, 47 + message: err.message, 48 + stack: err.stack, 49 + }, 50 + method: c.req.method, 51 + url: c.req.url, 52 + }); 53 + 54 + return c.json({ error: "internal server error" }, 500); 55 + }); 56 + 57 + app.all( 58 + "/mcp", 59 + cors({ 60 + origin: "*", 61 + allowMethods: ["GET", "POST", "DELETE", "OPTIONS"], 62 + allowHeaders: [ 63 + "Content-Type", 64 + "Authorization", 65 + "Mcp-Session-Id", 66 + "Mcp-Protocol-Version", 67 + ], 68 + exposeHeaders: ["Mcp-Session-Id"], 69 + }), 70 + ); 71 + 72 + app.post("/mcp", async (ctx) => { 73 + const sessionId = ctx.req.header("mcp-session-id") ?? crypto.randomUUID(); 74 + let session = sessions.get(sessionId); 75 + 76 + if (session) { 77 + logger.info("resuming session {sessionId}", { sessionId }); 78 + } else { 79 + logger.info("creating new session {sessionId}", { sessionId }); 80 + 81 + const server = createServer(); 82 + 83 + session = new StreamableHTTPServerTransport({ 84 + sessionIdGenerator: () => sessionId, 85 + }); 86 + 87 + session.onclose = () => { 88 + logger.info("closing session {sessionId}", { sessionId }); 89 + }; 90 + 91 + await server.connect(session); 92 + 93 + sessions.set(sessionId, session); 94 + } 95 + 96 + const { req, res } = toReqRes(ctx.req.raw); 97 + 98 + await session.handleRequest(req, res); 99 + 100 + return await toFetchResponse(res); 101 + }); 102 + 103 + app.on(["GET", "DELETE"], "/mcp", async (ctx) => { 104 + const sessionId = ctx.req.header("mcp-session-id") ?? ""; 105 + const session = sessions.get(sessionId); 106 + 107 + if (!session) { 108 + logger.info("{method} invalid session {sessionId}", { 109 + method: ctx.req.method, 110 + sessionId, 111 + }); 112 + 113 + return ctx.json({ error: "invalid or missing session" }, 401); 114 + } 115 + 116 + const { req, res } = toReqRes(ctx.req.raw); 117 + 118 + await session.handleRequest(req, res); 119 + 120 + return await toFetchResponse(res); 121 + }); 122 + 123 + return app; 124 + }
+18 -75
packages/mcp/index.ts
··· 1 1 import { parseArgs } from "@std/cli"; 2 + import { AsyncLocalStorage } from "node:async_hooks"; 2 3 import { configure, getConsoleSink, getLogger } from "@logtape/logtape"; 3 4 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 - import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 5 - import { toFetchResponse, toReqRes } from "fetch-to-node"; 6 5 7 6 import { createServer } from "./server.ts"; 7 + import { createApp } from "./hono.ts"; 8 8 9 9 async function main() { 10 10 await configure({ 11 11 sinks: { console: getConsoleSink() }, 12 12 loggers: [ 13 - { category: "cistern-mcp", lowestLevel: "trace", sinks: ["console"] }, 13 + { 14 + category: ["cistern", "mcp"], 15 + lowestLevel: "trace", 16 + sinks: ["console"], 17 + }, 18 + { 19 + category: ["cistern", "http"], 20 + lowestLevel: "info", 21 + sinks: ["console"], 22 + }, 14 23 ], 24 + contextLocalStorage: new AsyncLocalStorage(), 15 25 }); 16 26 17 - const logger = getLogger("cistern-mcp"); 27 + const logger = getLogger(["cistern", "mcp"]); 18 28 const args = parseArgs(Deno.args, { 19 29 boolean: ["http"], 20 30 }); 21 31 22 - const server = createServer(); 23 - 24 32 if (!args.http) { 25 33 logger.info("starting in stdio mode"); 26 34 27 35 const transport = new StdioServerTransport(); 36 + const server = createServer(); 37 + 28 38 await server.connect(transport); 29 39 } else { 30 40 logger.info("starting in streamable HTTP mode"); 31 41 32 - const sessions: Map<string, StreamableHTTPServerTransport> = new Map(); 42 + const app = createApp(); 33 43 34 44 Deno.serve( 35 45 { ··· 38 48 ...addr, 39 49 }); 40 50 }, 41 - onError(error) { 42 - logger.error( 43 - "unexpected route error: {error}", 44 - { error }, 45 - ); 46 - 47 - return new Response(null, { status: 500 }); 48 - }, 49 51 }, 50 - async function handler(request: Request): Promise<Response> { 51 - const PATH = new URLPattern({ pathname: "/mcp" }); 52 - 53 - if (!PATH.exec(request.url)) { 54 - logger.info("not found", { 55 - status: 404, 56 - url: request.url, 57 - }); 58 - 59 - return new Response(null, { status: 404 }); 60 - } 61 - 62 - const sessionId = request.headers.get("mcp-session-id"); 63 - let transport: StreamableHTTPServerTransport; 64 - 65 - if (sessionId && sessions.has(sessionId)) { 66 - logger.info("{method} resuming session {sessionId}", { 67 - sessionId, 68 - method: request.method, 69 - }); 70 - 71 - transport = sessions.get(sessionId)!; 72 - } else if ( 73 - request.method !== "POST" && !sessions.has(sessionId ?? "") 74 - ) { 75 - logger.error("{method} has invalid session {sessionId}", { 76 - sessionId, 77 - method: request.method, 78 - }); 79 - 80 - return Response.json({ error: "invalid or missing session" }, { 81 - status: 401, 82 - }); 83 - } else { 84 - const sessionId = crypto.randomUUID(); 85 - 86 - logger.info("opening new session {sessionId}", { sessionId }); 87 - 88 - transport = new StreamableHTTPServerTransport({ 89 - sessionIdGenerator: () => sessionId as string, 90 - }); 91 - 92 - transport.onclose = () => { 93 - logger.info("session {sessionId} closed, cleaning up", { 94 - sessionId, 95 - }); 96 - sessions.delete(sessionId); 97 - }; 98 - 99 - sessions.set(sessionId, transport); 100 - 101 - await server.connect(transport); 102 - } 103 - 104 - const { req, res } = toReqRes(request); 105 - 106 - await transport.handleRequest(req, res); 107 - 108 - return await toFetchResponse(res); 109 - }, 52 + app.fetch, 110 53 ); 111 54 } 112 55 }