Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
166591fc09 | ||
|
|
f64728774d | ||
|
|
631df2ff46 | ||
|
|
8f934a8f8e | ||
|
|
086a028879 |
No files matched your search
@@ -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"]
|
||||
@@ -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"
|
||||
@@ -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": {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user