readthemanual 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +206 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +134 -0
- package/dist/db.d.ts +33 -0
- package/dist/db.js +153 -0
- package/dist/ingest.d.ts +9 -0
- package/dist/ingest.js +163 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +105 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +111 -0
- package/docs-search/package-lock.json +1090 -0
- package/docs-search/package.json +30 -0
- package/docs-search/src/cli.ts +129 -0
- package/docs-search/src/db.ts +192 -0
- package/docs-search/src/ingest.ts +168 -0
- package/docs-search/src/server.ts +145 -0
- package/docs-search/tsconfig.json +17 -0
- package/package.json +32 -0
- package/src/cli.ts +221 -0
- package/src/config.ts +143 -0
- package/src/db.ts +207 -0
- package/src/ingest.ts +200 -0
- package/src/mcp.ts +124 -0
- package/src/server.ts +145 -0
- package/tsconfig.json +17 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.openDb = openDb;
|
|
7
|
+
exports.clearDocs = clearDocs;
|
|
8
|
+
exports.insertSection = insertSection;
|
|
9
|
+
exports.search = search;
|
|
10
|
+
exports.getDocument = getDocument;
|
|
11
|
+
exports.listDocuments = listDocuments;
|
|
12
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const os_1 = __importDefault(require("os"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const DEFAULT_DB_PATH = path_1.default.join(os_1.default.homedir(), ".local", "rtm", "rtm.db");
|
|
17
|
+
function openDb(dbPath = DEFAULT_DB_PATH) {
|
|
18
|
+
const dir = path_1.default.dirname(dbPath);
|
|
19
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
20
|
+
try {
|
|
21
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err.code === "EACCES") {
|
|
25
|
+
console.error(`Error: No write permission to ${dir}\n` +
|
|
26
|
+
`If installed globally, run with sudo or install locally.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const db = new better_sqlite3_1.default(dbPath);
|
|
33
|
+
db.pragma("journal_mode = WAL");
|
|
34
|
+
db.pragma("foreign_keys = ON");
|
|
35
|
+
initSchema(db);
|
|
36
|
+
return db;
|
|
37
|
+
}
|
|
38
|
+
function initSchema(db) {
|
|
39
|
+
db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS docs (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
file_path TEXT NOT NULL,
|
|
43
|
+
heading TEXT NOT NULL,
|
|
44
|
+
content TEXT NOT NULL,
|
|
45
|
+
language TEXT NOT NULL DEFAULT 'en'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_docs_path ON docs(file_path);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_docs_lang ON docs(language);
|
|
50
|
+
|
|
51
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5(
|
|
52
|
+
heading,
|
|
53
|
+
content,
|
|
54
|
+
content='docs',
|
|
55
|
+
content_rowid='id',
|
|
56
|
+
tokenize='porter unicode61'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE TRIGGER IF NOT EXISTS docs_ai AFTER INSERT ON docs BEGIN
|
|
60
|
+
INSERT INTO docs_fts(rowid, heading, content)
|
|
61
|
+
VALUES (new.id, new.heading, new.content);
|
|
62
|
+
END;
|
|
63
|
+
|
|
64
|
+
CREATE TRIGGER IF NOT EXISTS docs_ad AFTER DELETE ON docs BEGIN
|
|
65
|
+
INSERT INTO docs_fts(docs_fts, rowid, heading, content)
|
|
66
|
+
VALUES ('delete', old.id, old.heading, old.content);
|
|
67
|
+
END;
|
|
68
|
+
|
|
69
|
+
CREATE TRIGGER IF NOT EXISTS docs_au AFTER UPDATE ON docs BEGIN
|
|
70
|
+
INSERT INTO docs_fts(docs_fts, rowid, heading, content)
|
|
71
|
+
VALUES ('delete', old.id, old.heading, old.content);
|
|
72
|
+
INSERT INTO docs_fts(rowid, heading, content)
|
|
73
|
+
VALUES (new.id, new.heading, new.content);
|
|
74
|
+
END;
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
function clearDocs(db) {
|
|
78
|
+
db.exec("DELETE FROM docs");
|
|
79
|
+
}
|
|
80
|
+
function insertSection(db, section) {
|
|
81
|
+
const stmt = db.prepare("INSERT INTO docs (file_path, heading, content, language) VALUES (?, ?, ?, ?)");
|
|
82
|
+
const result = stmt.run(section.file_path, section.heading, section.content, section.language);
|
|
83
|
+
return Number(result.lastInsertRowid);
|
|
84
|
+
}
|
|
85
|
+
function sanitizeFtsQuery(query) {
|
|
86
|
+
const tokens = query
|
|
87
|
+
.replace(/"/g, "")
|
|
88
|
+
.split(/\s+/)
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
if (tokens.length === 0)
|
|
91
|
+
return '""';
|
|
92
|
+
return tokens.map((t) => `"${t}"`).join(" ");
|
|
93
|
+
}
|
|
94
|
+
function search(db, query, opts = {}) {
|
|
95
|
+
const limit = opts.limit ?? 20;
|
|
96
|
+
const ftsQuery = sanitizeFtsQuery(query);
|
|
97
|
+
let countSql = `
|
|
98
|
+
SELECT COUNT(*) as total
|
|
99
|
+
FROM docs_fts
|
|
100
|
+
JOIN docs d ON d.id = docs_fts.rowid
|
|
101
|
+
WHERE docs_fts MATCH ?
|
|
102
|
+
`;
|
|
103
|
+
let sql = `
|
|
104
|
+
SELECT
|
|
105
|
+
d.id, d.file_path, d.heading, d.content, d.language,
|
|
106
|
+
docs_fts.rank AS rank,
|
|
107
|
+
snippet(docs_fts, 1, '<mark>', '</mark>', '...', 40) AS snippet
|
|
108
|
+
FROM docs_fts
|
|
109
|
+
JOIN docs d ON d.id = docs_fts.rowid
|
|
110
|
+
WHERE docs_fts MATCH ?
|
|
111
|
+
`;
|
|
112
|
+
const params = [ftsQuery];
|
|
113
|
+
const countParams = [ftsQuery];
|
|
114
|
+
if (opts.sourcePrefix) {
|
|
115
|
+
countSql += " AND d.file_path LIKE ?";
|
|
116
|
+
sql += " AND d.file_path LIKE ?";
|
|
117
|
+
params.push(opts.sourcePrefix + "%");
|
|
118
|
+
countParams.push(opts.sourcePrefix + "%");
|
|
119
|
+
}
|
|
120
|
+
if (opts.language) {
|
|
121
|
+
countSql += " AND d.language = ?";
|
|
122
|
+
sql += " AND d.language = ?";
|
|
123
|
+
params.push(opts.language);
|
|
124
|
+
countParams.push(opts.language);
|
|
125
|
+
}
|
|
126
|
+
sql += " ORDER BY docs_fts.rank LIMIT ?";
|
|
127
|
+
params.push(limit);
|
|
128
|
+
const { total } = db.prepare(countSql).get(...countParams);
|
|
129
|
+
const results = db.prepare(sql).all(...params);
|
|
130
|
+
return { results, total };
|
|
131
|
+
}
|
|
132
|
+
function getDocument(db, id) {
|
|
133
|
+
return (db.prepare("SELECT * FROM docs WHERE id = ?").get(id) ?? null);
|
|
134
|
+
}
|
|
135
|
+
function listDocuments(db, opts = {}) {
|
|
136
|
+
const limit = opts.limit ?? 100;
|
|
137
|
+
const offset = opts.offset ?? 0;
|
|
138
|
+
let countSql = "SELECT COUNT(*) as total FROM docs";
|
|
139
|
+
let sql = "SELECT id, file_path, heading, language FROM docs";
|
|
140
|
+
const params = [];
|
|
141
|
+
const countParams = [];
|
|
142
|
+
if (opts.language) {
|
|
143
|
+
countSql += " WHERE language = ?";
|
|
144
|
+
sql += " WHERE language = ?";
|
|
145
|
+
params.push(opts.language);
|
|
146
|
+
countParams.push(opts.language);
|
|
147
|
+
}
|
|
148
|
+
sql += " ORDER BY file_path, id LIMIT ? OFFSET ?";
|
|
149
|
+
params.push(limit, offset);
|
|
150
|
+
const { total } = db.prepare(countSql).get(...countParams);
|
|
151
|
+
const docs = db.prepare(sql).all(...params);
|
|
152
|
+
return { docs, total };
|
|
153
|
+
}
|
package/dist/ingest.d.ts
ADDED
package/dist/ingest.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ingest = ingest;
|
|
7
|
+
const db_1 = require("./db");
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
async function fetchTree(repo, branch) {
|
|
12
|
+
const url = `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`;
|
|
13
|
+
const res = await fetch(url, {
|
|
14
|
+
headers: {
|
|
15
|
+
Accept: "application/vnd.github.v3+json",
|
|
16
|
+
...(process.env.GITHUB_TOKEN
|
|
17
|
+
? { Authorization: `token ${process.env.GITHUB_TOKEN}` }
|
|
18
|
+
: {}),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
throw new Error(`GitHub tree API: ${res.status} ${res.statusText}`);
|
|
23
|
+
const data = (await res.json());
|
|
24
|
+
return data.tree;
|
|
25
|
+
}
|
|
26
|
+
async function fetchRawFile(repo, branch, filePath) {
|
|
27
|
+
const url = `https://raw.githubusercontent.com/${repo}/${branch}/${filePath}`;
|
|
28
|
+
const res = await fetch(url);
|
|
29
|
+
if (!res.ok)
|
|
30
|
+
throw new Error(`Failed to fetch ${filePath}: ${res.status}`);
|
|
31
|
+
return res.text();
|
|
32
|
+
}
|
|
33
|
+
function detectLanguage(filePath) {
|
|
34
|
+
const match = filePath.match(/^docs\/i18n\/([a-z]{2}(?:-[A-Z]{2})?)\//);
|
|
35
|
+
if (match)
|
|
36
|
+
return match[1];
|
|
37
|
+
return "en";
|
|
38
|
+
}
|
|
39
|
+
function splitByHeadings(markdown) {
|
|
40
|
+
const lines = markdown.split("\n");
|
|
41
|
+
const sections = [];
|
|
42
|
+
let currentHeading = "(intro)";
|
|
43
|
+
let currentLines = [];
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
|
|
46
|
+
if (headingMatch) {
|
|
47
|
+
if (currentLines.length > 0) {
|
|
48
|
+
const content = currentLines.join("\n").trim();
|
|
49
|
+
if (content.length > 0) {
|
|
50
|
+
sections.push({ heading: currentHeading, content });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
currentHeading = headingMatch[2];
|
|
54
|
+
currentLines = [];
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
currentLines.push(line);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (currentLines.length > 0) {
|
|
61
|
+
const content = currentLines.join("\n").trim();
|
|
62
|
+
if (content.length > 0) {
|
|
63
|
+
sections.push({ heading: currentHeading, content });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return sections;
|
|
67
|
+
}
|
|
68
|
+
async function ingestSource(db, source, opts) {
|
|
69
|
+
const log = opts.verbose ? console.log : () => { };
|
|
70
|
+
const repo = source.repo;
|
|
71
|
+
const branch = source.branch ?? "main";
|
|
72
|
+
const docsPrefix = source.docsPrefix ?? "docs/";
|
|
73
|
+
const config = (0, config_1.loadConfig)();
|
|
74
|
+
const docsDir = (0, config_1.getDocsDir)(config, source.name);
|
|
75
|
+
log(`[${source.name}] Fetching tree from ${repo}@${branch}...`);
|
|
76
|
+
const tree = await fetchTree(repo, branch);
|
|
77
|
+
const mdFiles = tree.filter((entry) => {
|
|
78
|
+
if (entry.type !== "blob")
|
|
79
|
+
return false;
|
|
80
|
+
if (!entry.path.startsWith(docsPrefix))
|
|
81
|
+
return false;
|
|
82
|
+
if (!entry.path.endsWith(".md"))
|
|
83
|
+
return false;
|
|
84
|
+
if (/\.(jpg|png|svg|gif)$/i.test(entry.path))
|
|
85
|
+
return false;
|
|
86
|
+
if (!opts.allLangs) {
|
|
87
|
+
const lang = detectLanguage(entry.path);
|
|
88
|
+
if (lang !== "en")
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
log(`[${source.name}] Found ${mdFiles.length} markdown files.`);
|
|
94
|
+
// Save docs locally
|
|
95
|
+
fs_1.default.rmSync(docsDir, { recursive: true, force: true });
|
|
96
|
+
fs_1.default.mkdirSync(docsDir, { recursive: true });
|
|
97
|
+
let filesProcessed = 0;
|
|
98
|
+
let sectionsInserted = 0;
|
|
99
|
+
const allSections = [];
|
|
100
|
+
const batchSize = 10;
|
|
101
|
+
for (let i = 0; i < mdFiles.length; i += batchSize) {
|
|
102
|
+
const batch = mdFiles.slice(i, i + batchSize);
|
|
103
|
+
const results = await Promise.all(batch.map(async (entry) => {
|
|
104
|
+
const content = await fetchRawFile(repo, branch, entry.path);
|
|
105
|
+
return { path: entry.path, content };
|
|
106
|
+
}));
|
|
107
|
+
for (const { path: filePath, content } of results) {
|
|
108
|
+
// Strip docsPrefix so we don't get docs/docs/ nesting
|
|
109
|
+
const relativePath = filePath.startsWith(docsPrefix)
|
|
110
|
+
? filePath.slice(docsPrefix.length)
|
|
111
|
+
: filePath;
|
|
112
|
+
// Write raw markdown to disk
|
|
113
|
+
const localPath = path_1.default.join(docsDir, relativePath);
|
|
114
|
+
fs_1.default.mkdirSync(path_1.default.dirname(localPath), { recursive: true });
|
|
115
|
+
fs_1.default.writeFileSync(localPath, content, "utf-8");
|
|
116
|
+
const lang = detectLanguage(filePath);
|
|
117
|
+
const sections = splitByHeadings(content);
|
|
118
|
+
for (const section of sections) {
|
|
119
|
+
allSections.push({
|
|
120
|
+
file_path: `${source.name}/${relativePath}`,
|
|
121
|
+
heading: section.heading,
|
|
122
|
+
content: section.content,
|
|
123
|
+
language: lang,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
filesProcessed++;
|
|
127
|
+
log(` [${filesProcessed}/${mdFiles.length}] ${filePath} (${sections.length} sections)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const insertMany = db.transaction((sections) => {
|
|
131
|
+
for (const s of sections) {
|
|
132
|
+
(0, db_1.insertSection)(db, s);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
insertMany(allSections);
|
|
136
|
+
sectionsInserted = allSections.length;
|
|
137
|
+
return { filesProcessed, sectionsInserted };
|
|
138
|
+
}
|
|
139
|
+
async function ingest(opts) {
|
|
140
|
+
const config = (0, config_1.loadConfig)();
|
|
141
|
+
const dbPath = opts.dbPath ?? (0, config_1.getDbPath)(config);
|
|
142
|
+
const db = (0, db_1.openDb)(dbPath);
|
|
143
|
+
const log = opts.verbose ? console.log : () => { };
|
|
144
|
+
// Filter to a single source if requested
|
|
145
|
+
const sources = opts.source
|
|
146
|
+
? config.sources.filter((s) => s.name === opts.source)
|
|
147
|
+
: config.sources;
|
|
148
|
+
if (sources.length === 0) {
|
|
149
|
+
const available = config.sources.map((s) => s.name).join(", ");
|
|
150
|
+
throw new Error(`Source "${opts.source}" not found in config. Available: ${available}`);
|
|
151
|
+
}
|
|
152
|
+
(0, db_1.clearDocs)(db);
|
|
153
|
+
let totalFiles = 0;
|
|
154
|
+
let totalSections = 0;
|
|
155
|
+
for (const source of sources) {
|
|
156
|
+
const result = await ingestSource(db, source, opts);
|
|
157
|
+
totalFiles += result.filesProcessed;
|
|
158
|
+
totalSections += result.sectionsInserted;
|
|
159
|
+
}
|
|
160
|
+
db.close();
|
|
161
|
+
log(`Done. ${totalFiles} files, ${totalSections} sections indexed from ${sources.length} source(s).`);
|
|
162
|
+
return { filesProcessed: totalFiles, sectionsInserted: totalSections };
|
|
163
|
+
}
|
package/dist/mcp.d.ts
ADDED
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startMcp = startMcp;
|
|
7
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const db_1 = require("./db");
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
async function startMcp(opts) {
|
|
14
|
+
const config = (0, config_1.loadConfig)();
|
|
15
|
+
const dbPath = opts.dbPath ?? (0, config_1.getDbPath)(config);
|
|
16
|
+
const db = (0, db_1.openDb)(dbPath);
|
|
17
|
+
const server = new mcp_js_1.McpServer({
|
|
18
|
+
name: "rtm",
|
|
19
|
+
version: "0.0.1",
|
|
20
|
+
});
|
|
21
|
+
server.registerTool("search", {
|
|
22
|
+
title: "Search docs",
|
|
23
|
+
description: "Full-text search across indexed documentation. Returns matching sections with snippets.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
query: zod_1.z.string().describe("Search query"),
|
|
26
|
+
source: zod_1.z.string().optional().describe("Source name to scope search (default: last used source)"),
|
|
27
|
+
language: zod_1.z.string().optional().describe("Filter by language code (e.g. 'en')"),
|
|
28
|
+
limit: zod_1.z.number().optional().describe("Max results (default: 10)"),
|
|
29
|
+
},
|
|
30
|
+
}, async ({ query, source, language, limit }) => {
|
|
31
|
+
const cfg = (0, config_1.loadConfig)();
|
|
32
|
+
const sourceName = source ?? (0, config_1.getDefaultSource)(cfg);
|
|
33
|
+
const { results, total } = (0, db_1.search)(db, query, {
|
|
34
|
+
language,
|
|
35
|
+
limit: limit ?? 10,
|
|
36
|
+
sourcePrefix: sourceName ? `${sourceName}/` : undefined,
|
|
37
|
+
});
|
|
38
|
+
const text = results.length === 0
|
|
39
|
+
? "No results found."
|
|
40
|
+
: results.map((r) => {
|
|
41
|
+
const fullPath = path_1.default.join(cfg.dataDir, r.file_path);
|
|
42
|
+
const clean = r.snippet.replace(/<\/?mark>/g, "");
|
|
43
|
+
return `[${r.id}] ${r.heading}\n ${fullPath}\n ${clean}`;
|
|
44
|
+
}).join("\n\n") + `\n\n${results.length} of ${total} match(es)`;
|
|
45
|
+
return { content: [{ type: "text", text }] };
|
|
46
|
+
});
|
|
47
|
+
server.registerTool("get_document", {
|
|
48
|
+
title: "Get document",
|
|
49
|
+
description: "Retrieve a full document section by its ID.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
id: zod_1.z.number().describe("Document section ID"),
|
|
52
|
+
},
|
|
53
|
+
}, async ({ id }) => {
|
|
54
|
+
const doc = (0, db_1.getDocument)(db, id);
|
|
55
|
+
if (!doc) {
|
|
56
|
+
return { content: [{ type: "text", text: `Document ${id} not found.` }] };
|
|
57
|
+
}
|
|
58
|
+
const cfg = (0, config_1.loadConfig)();
|
|
59
|
+
const fullPath = path_1.default.join(cfg.dataDir, doc.file_path);
|
|
60
|
+
return {
|
|
61
|
+
content: [{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: `# ${doc.heading}\n${fullPath} (${doc.language})\n\n${doc.content}`,
|
|
64
|
+
}],
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
server.registerTool("list_sources", {
|
|
68
|
+
title: "List sources",
|
|
69
|
+
description: "List configured documentation sources.",
|
|
70
|
+
inputSchema: {},
|
|
71
|
+
}, async () => {
|
|
72
|
+
const cfg = (0, config_1.loadConfig)();
|
|
73
|
+
const defaultSrc = (0, config_1.getDefaultSource)(cfg);
|
|
74
|
+
const lines = cfg.sources.map((s) => {
|
|
75
|
+
const marker = s.name === defaultSrc ? " (default)" : "";
|
|
76
|
+
return `- ${s.name}: ${s.repo}@${s.branch ?? "main"}${marker}`;
|
|
77
|
+
});
|
|
78
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
79
|
+
});
|
|
80
|
+
server.registerTool("list_documents", {
|
|
81
|
+
title: "List documents",
|
|
82
|
+
description: "List indexed document sections.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
language: zod_1.z.string().optional().describe("Filter by language"),
|
|
85
|
+
limit: zod_1.z.number().optional().describe("Max results (default: 50)"),
|
|
86
|
+
offset: zod_1.z.number().optional().describe("Offset for pagination"),
|
|
87
|
+
},
|
|
88
|
+
}, async ({ language, limit, offset }) => {
|
|
89
|
+
const result = (0, db_1.listDocuments)(db, {
|
|
90
|
+
language,
|
|
91
|
+
limit: limit ?? 50,
|
|
92
|
+
offset: offset ?? 0,
|
|
93
|
+
});
|
|
94
|
+
const cfg = (0, config_1.loadConfig)();
|
|
95
|
+
const lines = result.docs.map((d) => {
|
|
96
|
+
const fullPath = path_1.default.join(cfg.dataDir, d.file_path);
|
|
97
|
+
return `[${d.id}] ${fullPath} — ${d.heading}`;
|
|
98
|
+
});
|
|
99
|
+
const text = lines.join("\n") + `\n\n${result.docs.length} of ${result.total} total`;
|
|
100
|
+
return { content: [{ type: "text", text }] };
|
|
101
|
+
});
|
|
102
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
103
|
+
await server.connect(transport);
|
|
104
|
+
console.error("rtm MCP server running on stdio");
|
|
105
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createApp = createApp;
|
|
7
|
+
exports.startServer = startServer;
|
|
8
|
+
const hono_1 = require("hono");
|
|
9
|
+
const node_server_1 = require("@hono/node-server");
|
|
10
|
+
const db_1 = require("./db");
|
|
11
|
+
function rpcError(id, code, message) {
|
|
12
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
13
|
+
}
|
|
14
|
+
function rpcResult(id, result) {
|
|
15
|
+
return { jsonrpc: "2.0", id, result };
|
|
16
|
+
}
|
|
17
|
+
function handleRpc(db, req) {
|
|
18
|
+
const { id, method, params } = req;
|
|
19
|
+
switch (method) {
|
|
20
|
+
case "search": {
|
|
21
|
+
const query = params?.query;
|
|
22
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
23
|
+
return rpcError(id, -32602, "params.query is required and must be a non-empty string");
|
|
24
|
+
}
|
|
25
|
+
const language = typeof params?.language === "string" ? params.language : undefined;
|
|
26
|
+
const limit = typeof params?.limit === "number" ? params.limit : undefined;
|
|
27
|
+
const { results, total } = (0, db_1.search)(db, query, { language, limit });
|
|
28
|
+
return rpcResult(id, { results, total, count: results.length });
|
|
29
|
+
}
|
|
30
|
+
case "get_document": {
|
|
31
|
+
const docId = params?.id;
|
|
32
|
+
if (typeof docId !== "number") {
|
|
33
|
+
return rpcError(id, -32602, "params.id is required and must be a number");
|
|
34
|
+
}
|
|
35
|
+
const doc = (0, db_1.getDocument)(db, docId);
|
|
36
|
+
if (!doc) {
|
|
37
|
+
return rpcError(id, -32602, `Document with id ${docId} not found`);
|
|
38
|
+
}
|
|
39
|
+
return rpcResult(id, doc);
|
|
40
|
+
}
|
|
41
|
+
case "list_documents": {
|
|
42
|
+
const language = typeof params?.language === "string" ? params.language : undefined;
|
|
43
|
+
const limit = typeof params?.limit === "number" ? params.limit : undefined;
|
|
44
|
+
const offset = typeof params?.offset === "number" ? params.offset : undefined;
|
|
45
|
+
const result = (0, db_1.listDocuments)(db, { language, limit, offset });
|
|
46
|
+
return rpcResult(id, result);
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
return rpcError(id, -32601, `Method '${method}' not found`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function createApp(db) {
|
|
53
|
+
const app = new hono_1.Hono();
|
|
54
|
+
app.post("/rpc", async (c) => {
|
|
55
|
+
let body;
|
|
56
|
+
try {
|
|
57
|
+
body = await c.req.json();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return c.json(rpcError(null, -32700, "Parse error"), 200);
|
|
61
|
+
}
|
|
62
|
+
// Batch support
|
|
63
|
+
if (Array.isArray(body)) {
|
|
64
|
+
const responses = body.map((req) => {
|
|
65
|
+
if (!isValidRequest(req)) {
|
|
66
|
+
return rpcError(req?.id ?? null, -32600, "Invalid JSON-RPC request");
|
|
67
|
+
}
|
|
68
|
+
return handleRpc(db, req);
|
|
69
|
+
});
|
|
70
|
+
return c.json(responses, 200);
|
|
71
|
+
}
|
|
72
|
+
if (!isValidRequest(body)) {
|
|
73
|
+
return c.json(rpcError(body?.id ?? null, -32600, "Invalid JSON-RPC request"), 200);
|
|
74
|
+
}
|
|
75
|
+
return c.json(handleRpc(db, body), 200);
|
|
76
|
+
});
|
|
77
|
+
// Health check
|
|
78
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
79
|
+
return app;
|
|
80
|
+
}
|
|
81
|
+
function isValidRequest(obj) {
|
|
82
|
+
if (typeof obj !== "object" || obj === null)
|
|
83
|
+
return false;
|
|
84
|
+
const r = obj;
|
|
85
|
+
return r.jsonrpc === "2.0" && typeof r.method === "string";
|
|
86
|
+
}
|
|
87
|
+
const net_1 = __importDefault(require("net"));
|
|
88
|
+
function isPortInUse(port) {
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const server = net_1.default.createServer();
|
|
91
|
+
server.once("error", () => resolve(true));
|
|
92
|
+
server.once("listening", () => {
|
|
93
|
+
server.close();
|
|
94
|
+
resolve(false);
|
|
95
|
+
});
|
|
96
|
+
server.listen(port);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function startServer(opts) {
|
|
100
|
+
const port = opts.port ?? 3777;
|
|
101
|
+
if (await isPortInUse(port)) {
|
|
102
|
+
console.error(`Port ${port} is already in use. Another instance may be running.`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const db = (0, db_1.openDb)(opts.dbPath);
|
|
106
|
+
const app = createApp(db);
|
|
107
|
+
(0, node_server_1.serve)({ fetch: app.fetch, port }, () => {
|
|
108
|
+
console.log(`rtm server listening on http://localhost:${port}`);
|
|
109
|
+
console.log(`JSON-RPC endpoint: POST http://localhost:${port}/rpc`);
|
|
110
|
+
});
|
|
111
|
+
}
|