Scaffold the Havre MCP server (walking skeleton) (#195)
Co-authored-by: Time <time@users.noreply.teahub.xyz>
This commit was merged in pull request #195.
This commit is contained in:
1 parent
26f64e2f00
commit
89070f46a4
7 files changed
+1281
No files matched your search
@@ -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 +1,2 @@
|
||||
__pycache__/
|
||||
node_modules/
|
||||
Generated
+1216
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { createServer } from "./server.js";
|
||||
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user