From 086a028879f5517dfcf4a4d563c69c4a343d4ae3 Mon Sep 17 00:00:00 2001 From: Time Date: Mon, 19 Oct 2026 16:15:06 +0000 Subject: [PATCH 1/5] card #98: update havre/src/http.js --- havre/src/http.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 havre/src/http.js diff --git a/havre/src/http.js b/havre/src/http.js new file mode 100644 index 0000000..41b1024 --- /dev/null +++ b/havre/src/http.js @@ -0,0 +1,26 @@ +import { createServer as createHttp } from "node:http"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createServer } from "./server.js"; + +// Stateless streamable HTTP: a fresh MCP server + transport per request, nothing kept between requests. +export function createHttpServer() { + return createHttp(async (req, res) => { + const path = new URL(req.url, "http://x").pathname; + if (path === "/health") return res.writeHead(200, { "content-type": "text/plain" }).end("ok"); + if (path !== "/mcp") return res.writeHead(404).end(); + if (req.method !== "POST") return res.writeHead(405, { allow: "POST" }).end(); + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }); + const server = createServer(); + res.on("close", () => { transport.close(); server.close(); }); + try { + await server.connect(transport); + await transport.handleRequest(req, res); + } catch { + if (!res.headersSent) res.writeHead(500).end(); + } + }); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + createHttpServer().listen(Number(process.env.PORT ?? 8080), "0.0.0.0"); +} -- 2.54.0 From 8f934a8f8e680f121736604c1bf0a5f949213f39 Mon Sep 17 00:00:00 2001 From: Time Date: Mon, 19 Oct 2026 16:15:06 +0000 Subject: [PATCH 2/5] card #98: update havre/test/http.test.js --- havre/test/http.test.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 havre/test/http.test.js diff --git a/havre/test/http.test.js b/havre/test/http.test.js new file mode 100644 index 0000000..1100609 --- /dev/null +++ b/havre/test/http.test.js @@ -0,0 +1,22 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { createHttpServer } from "../src/http.js"; + +test("health check and MCP over streamable HTTP", async () => { + const http = createHttpServer().listen(0); + await new Promise((r) => http.once("listening", r)); + const base = `http://127.0.0.1:${http.address().port}`; + const client = new Client({ name: "test", version: "0.0.0" }); + try { + assert.equal((await fetch(`${base}/health`)).status, 200); + assert.equal((await fetch(`${base}/nope`)).status, 404); + await client.connect(new StreamableHTTPClientTransport(new URL(`${base}/mcp`))); + assert.equal(client.getServerVersion().name, "havre"); + assert.equal((await client.callTool({ name: "havre_ping", arguments: { message: "hi" } })).content[0].text, "hi"); + } finally { + await client.close(); + http.close(); + } +}); -- 2.54.0 From 631df2ff460301716165c528df71ec5c5c40302a Mon Sep 17 00:00:00 2001 From: Time Date: Mon, 19 Oct 2026 16:15:07 +0000 Subject: [PATCH 3/5] card #98: update havre/Dockerfile --- havre/Dockerfile | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 havre/Dockerfile diff --git a/havre/Dockerfile b/havre/Dockerfile new file mode 100644 index 0000000..ca59313 --- /dev/null +++ b/havre/Dockerfile @@ -0,0 +1,7 @@ +FROM node:22-alpine +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev +COPY src ./src +USER node +CMD ["node", "src/http.js"] -- 2.54.0 From f64728774d7b6607dcd29b67b773b8c3a6977270 Mon Sep 17 00:00:00 2001 From: Time Date: Mon, 19 Oct 2026 16:15:08 +0000 Subject: [PATCH 4/5] card #98: update havre/fly.toml --- havre/fly.toml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 havre/fly.toml diff --git a/havre/fly.toml b/havre/fly.toml new file mode 100644 index 0000000..35110ef --- /dev/null +++ b/havre/fly.toml @@ -0,0 +1,25 @@ +# Runs on the existing green-tea-mcp app. Deploy: cd havre && fly deploy (needs FLY_API_TOKEN). +app = "green-tea-mcp" + +[build] + +[env] + PORT = "8080" + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = "stop" # stateless, so idle machines can stop: no cost while unused + auto_start_machines = true + min_machines_running = 0 + + [[http_service.checks]] + method = "GET" + path = "/health" + interval = "30s" + timeout = "5s" + grace_period = "10s" + +[[vm]] + size = "shared-cpu-1x" + memory = "256mb" -- 2.54.0 From 166591fc0947321b704b44864f19c15eaf4902a9 Mon Sep 17 00:00:00 2001 From: Time Date: Mon, 19 Oct 2026 16:15:09 +0000 Subject: [PATCH 5/5] card #98: update havre/package.json --- havre/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/havre/package.json b/havre/package.json index 8722fff..0875d02 100644 --- a/havre/package.json +++ b/havre/package.json @@ -6,6 +6,7 @@ "description": "Havre MCP server. Stateless: no disk, no db, no stored secrets.", "scripts": { "start": "node src/index.js", + "start:http": "node src/http.js", "test": "node --test" }, "dependencies": { -- 2.54.0