+1
deno.jsonc
+1
deno.jsonc
+3
-1
packages/mcp/deno.jsonc
+3
-1
packages/mcp/deno.jsonc
+18
-20
packages/mcp/hono.ts
+18
-20
packages/mcp/hono.ts
···
1
1
import { Hono } from "hono";
2
2
import { cors } from "hono/cors";
3
3
import { bearerAuth } from "hono/bearer-auth";
4
-
import { createConsumer } from "@cistern/consumer";
5
4
import { getLogger, withContext } from "@logtape/logtape";
6
5
import { toFetchResponse, toReqRes } from "fetch-to-node";
7
6
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
···
20
19
return Deno.exit(1);
21
20
}
22
21
23
-
app.use("*", bearerAuth({ token }));
22
+
app.all(
23
+
"/mcp",
24
+
cors({
25
+
origin: "*",
26
+
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
27
+
allowHeaders: [
28
+
"Content-Type",
29
+
"Authorization",
30
+
"Mcp-Session-Id",
31
+
"Mcp-Protocol-Version",
32
+
],
33
+
exposeHeaders: ["Mcp-Session-Id"],
34
+
}),
35
+
);
36
+
37
+
app.use("/mcp", bearerAuth({ token }));
24
38
app.use("*", async (c, next) => {
25
39
const requestId = crypto.randomUUID();
26
40
const startTime = Date.now();
···
52
66
});
53
67
54
68
app.onError((err, c) => {
55
-
logger.error("request error", {
69
+
logger.error("request error {error}", {
56
70
error: {
57
71
name: err.name,
58
72
message: err.message,
···
65
79
return c.json({ error: "internal server error" }, 500);
66
80
});
67
81
68
-
app.all(
69
-
"/mcp",
70
-
cors({
71
-
origin: "*",
72
-
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
73
-
allowHeaders: [
74
-
"Content-Type",
75
-
"Authorization",
76
-
"Mcp-Session-Id",
77
-
"Mcp-Protocol-Version",
78
-
],
79
-
exposeHeaders: ["Mcp-Session-Id"],
80
-
}),
81
-
);
82
-
83
82
app.post("/mcp", async (ctx) => {
84
83
const sessionId = ctx.req.header("mcp-session-id") ?? crypto.randomUUID();
85
84
let session = sessions.get(sessionId);
···
90
89
logger.info("creating new session {sessionId}", { sessionId });
91
90
92
91
const options = collectOptions();
93
-
const consumer = await createConsumer(options);
94
-
const server = createServer(consumer);
92
+
const server = await createServer(options);
95
93
96
94
session = new StreamableHTTPServerTransport({
97
95
sessionIdGenerator: () => sessionId,
+1
-3
packages/mcp/index.ts
+1
-3
packages/mcp/index.ts
···
1
1
import { parseArgs } from "@std/cli";
2
-
import { createConsumer } from "@cistern/consumer";
3
2
import { AsyncLocalStorage } from "node:async_hooks";
4
3
import { configure, getConsoleSink, getLogger } from "@logtape/logtape";
5
4
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
···
35
34
logger.info("starting in stdio mode");
36
35
37
36
const options = collectOptions();
38
-
const consumer = await createConsumer(options);
37
+
const server = await createServer(options);
39
38
const transport = new StdioServerTransport();
40
-
const server = createServer(consumer);
41
39
42
40
await server.connect(transport);
43
41
} else {
+45
packages/mcp/kv.ts
+45
packages/mcp/kv.ts
···
1
+
import type { InputLocalKeyPair } from "@cistern/consumer";
2
+
import { getLogger } from "@logtape/logtape";
3
+
4
+
const KV_PATH = "./cistern-mcp.db";
5
+
6
+
let kv: Deno.Kv | undefined;
7
+
8
+
async function getKv(): Promise<Deno.Kv> {
9
+
if (!kv) {
10
+
kv = await Deno.openKv(KV_PATH);
11
+
}
12
+
return kv;
13
+
}
14
+
15
+
export async function getStoredKeypair(
16
+
handle: string,
17
+
): Promise<InputLocalKeyPair | null> {
18
+
const logger = getLogger(["cistern", "mcp"]);
19
+
const db = await getKv();
20
+
const result = await db.get<InputLocalKeyPair>([
21
+
"cistern",
22
+
"keypairs",
23
+
handle,
24
+
]);
25
+
26
+
if (result.value) {
27
+
logger.debug("found stored keypair for {handle}", { handle });
28
+
return result.value;
29
+
}
30
+
31
+
logger.debug("no stored keypair found for {handle}", { handle });
32
+
return null;
33
+
}
34
+
35
+
export async function storeKeypair(
36
+
handle: string,
37
+
keypair: InputLocalKeyPair,
38
+
): Promise<void> {
39
+
const logger = getLogger(["cistern", "mcp"]);
40
+
const db = await getKv();
41
+
42
+
await db.set(["cistern", "keypairs", handle], keypair);
43
+
44
+
logger.info("stored keypair for {handle}", { handle });
45
+
}
+46
-2
packages/mcp/server.ts
+46
-2
packages/mcp/server.ts
···
1
1
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
import { getLogger } from "@logtape/logtape";
3
3
import { z } from "zod";
4
-
import type { Consumer, DecryptedMemo } from "@cistern/consumer";
4
+
import type {
5
+
Consumer,
6
+
ConsumerOptions,
7
+
DecryptedMemo,
8
+
} from "@cistern/consumer";
9
+
import { createConsumer } from "@cistern/consumer";
10
+
import { serializeKey } from "@cistern/crypto";
11
+
import { getStoredKeypair, storeKeypair } from "./kv.ts";
5
12
6
-
export function createServer(consumer: Consumer) {
13
+
export 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
+
50
+
function _createServerWithConsumer(consumer: Consumer) {
7
51
const logger = getLogger("cistern-mcp");
8
52
const server = new McpServer({
9
53
name: "cistern-mcp",