Scaffold the Havre MCP server (walking skeleton) (#195)
time/worker-run cd havre && npm ci >/dev/null 2>&1; cd node_modules/@modelcontextprotocol/sdk && sed -n 20,60p dist/
ci / test (push) Successful in 42s
sync / sync (push) Successful in 5s

Co-authored-by: Time <time@users.noreply.teahub.xyz>
This commit was merged in pull request #195.
This commit is contained in:
time authored and time committed 2026-10-19 16:14:10 +00:00
1 parent 26f64e2f00
commit 89070f46a4
7 files changed
+1281

No files matched your search

+10
View File
@@ -870,6 +870,16 @@ if _sh.which("node"):
assert res.returncode == 0 and "aicharts: all good" in res.stdout, res.stdout[-2000:] + res.stderr[-2000:]
print("ALL OK 41")
# the Havre MCP server (havre/): handshake and tool over stdio. CI's only Python step runs this, so it installs the locked deps here
if _sh.which("npm"):
hv = str(Path(__file__).resolve().parents[2] / "havre")
for cmd in (["npm", "ci", "--no-audit", "--no-fund"], ["npm", "test"]):
res = _sp.run(cmd, cwd=hv, capture_output=True, text=True, timeout=180)
assert res.returncode == 0, " ".join(cmd) + "\n" + res.stdout[-2000:] + res.stderr[-2000:]
print("ALL OK havre")
else:
print("skipped the havre test: no npm")
# 78 Time retries its own parked cards (error / stuck), within TIME_AUTO_RETRY; a human only hears once it has given up
K78 = {**K, "TIME_WORKER": "cursor"}
meta78 = lambda w: [json.loads(re.search(r"<!-- time (.*) -->", t).group(1)) for _, t in w.posted]
+1
View File
@@ -1 +1,2 @@
__pycache__/
node_modules/
+1216
View File
File diff suppressed because it is too large. Load diff
+15
View File
@@ -0,0 +1,15 @@
{
"name": "havre-mcp",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Havre MCP server. Stateless: no disk, no db, no stored secrets.",
"scripts": {
"start": "node src/index.js",
"test": "node --test"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.32.1",
"zod": "4.6.7"
}
}
+4
View File
@@ -0,0 +1,4 @@
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server.js";
await createServer().connect(new StdioServerTransport());
+14
View File
@@ -0,0 +1,14 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// Stateless by construction: a fresh server per connection, handlers are pure functions of their input.
// Add tools here; nothing in this file may read or write disk, a database, or a stored secret.
export function createServer() {
const server = new McpServer({ name: "havre", version: "0.0.0" });
server.registerTool(
"havre_ping",
{ description: "Placeholder tool: echoes a message back (default 'pong').", inputSchema: { message: z.string().max(200).optional() } },
async ({ message }) => ({ content: [{ type: "text", text: message ?? "pong" }] }),
);
return server;
}
+21
View File
@@ -0,0 +1,21 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
// Real process over stdio: the same handshake an MCP host does.
test("handshake, tool list and havre_ping over stdio", async () => {
const client = new Client({ name: "test", version: "0.0.0" });
await client.connect(new StdioClientTransport({ command: process.execPath, args: [fileURLToPath(new URL("../src/index.js", import.meta.url))] }));
try {
assert.equal(client.getServerVersion().name, "havre");
assert.deepEqual((await client.listTools()).tools.map((t) => t.name), ["havre_ping"]);
assert.equal((await client.callTool({ name: "havre_ping", arguments: {} })).content[0].text, "pong");
assert.equal((await client.callTool({ name: "havre_ping", arguments: { message: "hi" } })).content[0].text, "hi");
const bad = await client.callTool({ name: "havre_ping", arguments: { message: 5 } });
assert.equal(bad.isError, true, "bad input is refused");
} finally {
await client.close();
}
});