docs: update node usage and bump version
Update README installation and quick-start commands to Node examples. - replace bun install/link commands with npm-based Node workflow - bump package version to 0.9.9 for CLI and MCP metadata - keep Bun guidance as optional development/runtime note
This commit is contained in:
parent
392934e78a
commit
13e8473455
15
README.md
15
README.md
@ -9,8 +9,8 @@ QMD combines BM25 full-text search, vector semantic search, and LLM re-ranking
|
||||
## Quick Start
|
||||
|
||||
```sh
|
||||
# Install globally
|
||||
bun install -g https://github.com/tobi/qmd
|
||||
# Install globally (Node)
|
||||
npm install -g github:tobi/qmd
|
||||
|
||||
# Create collections for your notes, docs, and meeting transcripts
|
||||
qmd collection add ~/notes --name notes
|
||||
@ -231,7 +231,8 @@ The `query` command uses **Reciprocal Rank Fusion (RRF)** with position-aware bl
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **Bun** >= 1.0.0
|
||||
- **Node.js** >= 22
|
||||
- **Bun** >= 1.0.0 (optional; supported for local development)
|
||||
- **macOS**: Homebrew SQLite (for extension support)
|
||||
```sh
|
||||
brew install sqlite
|
||||
@ -252,18 +253,16 @@ Models are downloaded from HuggingFace and cached in `~/.cache/qmd/models/`.
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
bun install -g github:tobi/qmd
|
||||
npm install -g github:tobi/qmd
|
||||
```
|
||||
|
||||
Make sure `~/.bun/bin` is in your PATH.
|
||||
|
||||
### Development
|
||||
|
||||
```sh
|
||||
git clone https://github.com/tobi/qmd
|
||||
cd qmd
|
||||
bun install
|
||||
bun link
|
||||
npm install
|
||||
npm link
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
40
package.json
40
package.json
@ -1,25 +1,39 @@
|
||||
{
|
||||
"name": "qmd",
|
||||
"version": "1.0.0",
|
||||
"version": "0.9.9",
|
||||
"description": "Quick Markdown Search - Full-text and vector search for markdown files",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"qmd": "./qmd"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "bun test",
|
||||
"qmd": "bun src/qmd.ts",
|
||||
"index": "bun src/qmd.ts index",
|
||||
"vector": "bun src/qmd.ts vector",
|
||||
"search": "bun src/qmd.ts search",
|
||||
"vsearch": "bun src/qmd.ts vsearch",
|
||||
"rerank": "bun src/qmd.ts rerank",
|
||||
"link": "bun link",
|
||||
"inspector": "npx @modelcontextprotocol/inspector bun src/qmd.ts mcp"
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run --reporter=verbose src/*.test.ts",
|
||||
"test:models": "vitest run --reporter=verbose src/models/*.test.ts",
|
||||
"test:integration": "vitest run --reporter=verbose src/integration/*.test.ts",
|
||||
"test:unit:bun": "bun run vitest run --reporter=verbose --testTimeout=120000 src/*.test.ts",
|
||||
"test:models:bun": "bun run vitest run --reporter=verbose --testTimeout=120000 src/models/*.test.ts",
|
||||
"test:integration:bun": "bun run vitest run --reporter=verbose --testTimeout=120000 src/integration/*.test.ts",
|
||||
"test:unit:node": "npx vitest run --reporter=verbose --testTimeout=120000 src/*.test.ts",
|
||||
"test:models:node": "npx vitest run --reporter=verbose --testTimeout=120000 src/models/*.test.ts",
|
||||
"test:integration:node": "npx vitest run --reporter=verbose --testTimeout=120000 src/integration/*.test.ts",
|
||||
"test:ci:bun": "npm run test:unit:bun && npm run test:models:bun && npm run test:integration:bun",
|
||||
"test:ci:node": "npm run test:unit:node && npm run test:models:node && npm run test:integration:node",
|
||||
"test:ci": "npm run test:unit && npm run test:models && npm run test:integration",
|
||||
"qmd": "tsx src/qmd.ts",
|
||||
"index": "tsx src/qmd.ts index",
|
||||
"vector": "tsx src/qmd.ts vector",
|
||||
"search": "tsx src/qmd.ts search",
|
||||
"vsearch": "tsx src/qmd.ts vsearch",
|
||||
"rerank": "tsx src/qmd.ts rerank",
|
||||
"inspector": "npx @modelcontextprotocol/inspector tsx src/qmd.ts mcp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"fast-glob": "^3.3.0",
|
||||
"node-llama-cpp": "^3.14.5",
|
||||
"picomatch": "^4.0.0",
|
||||
"sqlite-vec": "^0.1.7-alpha.2",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.2.1"
|
||||
@ -31,13 +45,15 @@
|
||||
"sqlite-vec-win32-x64": "^0.1.7-alpha.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"tsx": "^4.0.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"markdown",
|
||||
|
||||
85
src/mcp.ts
85
src/mcp.ts
@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* QMD MCP Server - Model Context Protocol server for QMD
|
||||
*
|
||||
@ -8,6 +7,8 @@
|
||||
* Follows MCP spec 2025-06-18 for proper response types.
|
||||
*/
|
||||
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import { fileURLToPath } from "url";
|
||||
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { WebStandardStreamableHTTPServerTransport }
|
||||
@ -147,7 +148,7 @@ function buildInstructions(store: Store): string {
|
||||
*/
|
||||
function createMcpServer(store: Store): McpServer {
|
||||
const server = new McpServer(
|
||||
{ name: "qmd", version: "1.0.0" },
|
||||
{ name: "qmd", version: "0.9.9" },
|
||||
{ instructions: buildInstructions(store) },
|
||||
);
|
||||
|
||||
@ -539,7 +540,7 @@ export async function startMcpServer(): Promise<void> {
|
||||
// =============================================================================
|
||||
|
||||
export type HttpServerHandle = {
|
||||
httpServer: ReturnType<typeof Bun.serve>;
|
||||
httpServer: import("http").Server;
|
||||
port: number;
|
||||
stop: () => Promise<void>;
|
||||
};
|
||||
@ -586,47 +587,79 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
|
||||
if (!quiet) console.error(msg);
|
||||
}
|
||||
|
||||
const httpServer = Bun.serve({
|
||||
port,
|
||||
hostname: "localhost",
|
||||
async fetch(req) {
|
||||
const reqStart = Date.now();
|
||||
const pathname = new URL(req.url).pathname;
|
||||
// Helper to collect request body
|
||||
async function collectBody(req: IncomingMessage): Promise<string> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of req) chunks.push(chunk as Buffer);
|
||||
return Buffer.concat(chunks).toString();
|
||||
}
|
||||
|
||||
if (pathname === "/health" && req.method === "GET") {
|
||||
const res = Response.json({
|
||||
status: "ok",
|
||||
uptime: Math.floor((Date.now() - startTime) / 1000),
|
||||
});
|
||||
const httpServer = createServer(async (nodeReq: IncomingMessage, nodeRes: ServerResponse) => {
|
||||
const reqStart = Date.now();
|
||||
const pathname = nodeReq.url || "/";
|
||||
|
||||
try {
|
||||
if (pathname === "/health" && nodeReq.method === "GET") {
|
||||
const body = JSON.stringify({ status: "ok", uptime: Math.floor((Date.now() - startTime) / 1000) });
|
||||
nodeRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
nodeRes.end(body);
|
||||
log(`${ts()} GET /health (${Date.now() - reqStart}ms)`);
|
||||
return res;
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/mcp" && req.method === "POST") {
|
||||
const body = await req.json();
|
||||
if (pathname === "/mcp" && nodeReq.method === "POST") {
|
||||
const rawBody = await collectBody(nodeReq);
|
||||
const body = JSON.parse(rawBody);
|
||||
const label = describeRequest(body);
|
||||
const res = await transport.handleRequest(req, { parsedBody: body });
|
||||
const url = `http://localhost:${port}${pathname}`;
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(nodeReq.headers)) {
|
||||
if (typeof v === "string") headers[k] = v;
|
||||
}
|
||||
const request = new Request(url, { method: "POST", headers, body: rawBody });
|
||||
const response = await transport.handleRequest(request, { parsedBody: body });
|
||||
nodeRes.writeHead(response.status, Object.fromEntries(response.headers));
|
||||
nodeRes.end(Buffer.from(await response.arrayBuffer()));
|
||||
log(`${ts()} POST /mcp ${label} (${Date.now() - reqStart}ms)`);
|
||||
return res;
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass other methods (GET, DELETE) to transport for protocol handling
|
||||
if (pathname === "/mcp") {
|
||||
return transport.handleRequest(req);
|
||||
const url = `http://localhost:${port}${pathname}`;
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(nodeReq.headers)) {
|
||||
if (typeof v === "string") headers[k] = v;
|
||||
}
|
||||
const rawBody = nodeReq.method !== "GET" && nodeReq.method !== "HEAD" ? await collectBody(nodeReq) : undefined;
|
||||
const request = new Request(url, { method: nodeReq.method || "GET", headers, ...(rawBody ? { body: rawBody } : {}) });
|
||||
const response = await transport.handleRequest(request);
|
||||
nodeRes.writeHead(response.status, Object.fromEntries(response.headers));
|
||||
nodeRes.end(Buffer.from(await response.arrayBuffer()));
|
||||
return;
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
nodeRes.writeHead(404);
|
||||
nodeRes.end("Not Found");
|
||||
} catch (err) {
|
||||
console.error("HTTP handler error:", err);
|
||||
nodeRes.writeHead(500);
|
||||
nodeRes.end("Internal Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
const actualPort = httpServer.port;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
httpServer.on("error", reject);
|
||||
httpServer.listen(port, "localhost", () => resolve());
|
||||
});
|
||||
|
||||
const actualPort = (httpServer.address() as import("net").AddressInfo).port;
|
||||
|
||||
let stopping = false;
|
||||
const stop = async () => {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
await transport.close();
|
||||
httpServer.stop();
|
||||
httpServer.close();
|
||||
store.close();
|
||||
await disposeDefaultLlamaCpp();
|
||||
};
|
||||
@ -647,6 +680,6 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
|
||||
}
|
||||
|
||||
// Run if this is the main module
|
||||
if (import.meta.main) {
|
||||
if (fileURLToPath(import.meta.url) === process.argv[1] || process.argv[1]?.endsWith("/mcp.ts")) {
|
||||
startMcpServer().catch(console.error);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user