+5
-6
deno.lock
+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
+1
packages/mcp/deno.jsonc
+124
packages/mcp/hono.ts
+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
+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
}