Compare commits

...
5 Commits
Author SHA1 Message Date
time 166591fc09 card #98: update havre/package.json
time/worker-run cd havre && npm ci >/dev/null 2>&1 && npm test 2>&1 | tail -20; (PORT=8099 node src/http.js & sleep
ci / test (pull_request) Successful in 43s
heartbeat / tick (pull_request_review) Canceled after 0s
2026-10-19 16:15:09 +00:00
time f64728774d card #98: update havre/fly.toml 2026-10-19 16:15:08 +00:00
time 631df2ff46 card #98: update havre/Dockerfile 2026-10-19 16:15:07 +00:00
time 8f934a8f8e card #98: update havre/test/http.test.js 2026-10-19 16:15:06 +00:00
time 086a028879 card #98: update havre/src/http.js 2026-10-19 16:15:06 +00:00
5 changed files with 81 additions and 0 deletions

No files matched your search

+7
View File
@@ -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"]
+25
View File
@@ -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"
+1
View File
@@ -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": {
+26
View File
@@ -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");
}
+22
View File
@@ -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();
}
});