@diegoaltoworks/chatter 0.1.8 → 0.8.0
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 +164 -106
- package/dist/auth/apikeys.test.d.ts +2 -0
- package/dist/auth/apikeys.test.d.ts.map +1 -0
- package/dist/core/guardrails.test.d.ts +2 -0
- package/dist/core/guardrails.test.d.ts.map +1 -0
- package/dist/core/llm.d.ts +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/loaders.test.d.ts +2 -0
- package/dist/core/loaders.test.d.ts.map +1 -0
- package/dist/core/prompts.test.d.ts +2 -0
- package/dist/core/prompts.test.d.ts.map +1 -0
- package/dist/core/session.test.d.ts +2 -0
- package/dist/core/session.test.d.ts.map +1 -0
- package/dist/core/widgets.test.d.ts +2 -0
- package/dist/core/widgets.test.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22638 -1395
- package/dist/index.mjs +22619 -1373
- package/dist/mcp-server/conversation-id.d.ts +27 -0
- package/dist/mcp-server/conversation-id.d.ts.map +1 -0
- package/dist/mcp-server/conversation-id.test.d.ts +2 -0
- package/dist/mcp-server/conversation-id.test.d.ts.map +1 -0
- package/dist/mcp-server/cost-tracker.d.ts +36 -0
- package/dist/mcp-server/cost-tracker.d.ts.map +1 -0
- package/dist/mcp-server/cost-tracker.test.d.ts +2 -0
- package/dist/mcp-server/cost-tracker.test.d.ts.map +1 -0
- package/dist/mcp-server/index.d.ts +12 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/logger.d.ts +25 -0
- package/dist/mcp-server/logger.d.ts.map +1 -0
- package/dist/mcp-server/logger.test.d.ts +2 -0
- package/dist/mcp-server/logger.test.d.ts.map +1 -0
- package/dist/mcp-server/rate-limiter.d.ts +59 -0
- package/dist/mcp-server/rate-limiter.d.ts.map +1 -0
- package/dist/mcp-server/rate-limiter.test.d.ts +2 -0
- package/dist/mcp-server/rate-limiter.test.d.ts.map +1 -0
- package/dist/mcp-server/types.d.ts +82 -0
- package/dist/mcp-server/types.d.ts.map +1 -0
- package/dist/mcp-server.d.ts +36 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +671 -0
- package/dist/mcp-server.mjs +636 -0
- package/dist/middleware/auth.test.d.ts +2 -0
- package/dist/middleware/auth.test.d.ts.map +1 -0
- package/dist/middleware/cors.test.d.ts +2 -0
- package/dist/middleware/cors.test.d.ts.map +1 -0
- package/dist/middleware/jwt.test.d.ts +2 -0
- package/dist/middleware/jwt.test.d.ts.map +1 -0
- package/dist/middleware/ratelimit.test.d.ts +2 -0
- package/dist/middleware/ratelimit.test.d.ts.map +1 -0
- package/dist/middleware/referrer.d.ts.map +1 -1
- package/dist/middleware/referrer.test.d.ts +2 -0
- package/dist/middleware/referrer.test.d.ts.map +1 -0
- package/dist/middleware/session.test.d.ts +2 -0
- package/dist/middleware/session.test.d.ts.map +1 -0
- package/package.json +26 -10
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/mcp-server.ts
|
|
31
|
+
var mcp_server_exports = {};
|
|
32
|
+
__export(mcp_server_exports, {
|
|
33
|
+
createMCPServer: () => createMCPServer
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(mcp_server_exports);
|
|
36
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
37
|
+
var import_openai = __toESM(require("openai"));
|
|
38
|
+
var import_zod = require("zod");
|
|
39
|
+
|
|
40
|
+
// src/core/guardrails.ts
|
|
41
|
+
function detectLeakage(text) {
|
|
42
|
+
const flags = [
|
|
43
|
+
/system prompt/i,
|
|
44
|
+
/hidden instruction/i,
|
|
45
|
+
/here (are|is) (my|the) rules/i,
|
|
46
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
47
|
+
/tool instructions/i
|
|
48
|
+
];
|
|
49
|
+
return flags.some((rx) => rx.test(text));
|
|
50
|
+
}
|
|
51
|
+
function scrubOutput(text) {
|
|
52
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/core/llm.ts
|
|
56
|
+
async function completeOnce({
|
|
57
|
+
client,
|
|
58
|
+
system,
|
|
59
|
+
messages,
|
|
60
|
+
temperature = 0.2
|
|
61
|
+
}) {
|
|
62
|
+
const res = await client.chat.completions.create({
|
|
63
|
+
model: "gpt-4o",
|
|
64
|
+
temperature,
|
|
65
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
66
|
+
});
|
|
67
|
+
let text = res.choices[0]?.message?.content ?? "";
|
|
68
|
+
if (detectLeakage(text))
|
|
69
|
+
text = "Sorry, I can't share internal instructions. How else can I help?";
|
|
70
|
+
return {
|
|
71
|
+
content: scrubOutput(text),
|
|
72
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/core/prompts.ts
|
|
77
|
+
var import_node_fs = require("node:fs");
|
|
78
|
+
var import_node_path = require("node:path");
|
|
79
|
+
var PromptLoader = class {
|
|
80
|
+
constructor(promptsDir, bot) {
|
|
81
|
+
this.promptsDir = promptsDir;
|
|
82
|
+
this.bot = bot;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Load a prompt template and interpolate variables
|
|
86
|
+
*/
|
|
87
|
+
load(filename) {
|
|
88
|
+
const path = (0, import_node_path.join)(this.promptsDir, filename);
|
|
89
|
+
const template = (0, import_node_fs.readFileSync)(path, "utf-8");
|
|
90
|
+
const personFirstName = this.bot.personName.split(" ")[0];
|
|
91
|
+
return template.replace(/\{\{botName\}\}/g, this.bot.name).replace(/\{\{personName\}\}/g, this.bot.personName).replace(/\{\{personFirstName\}\}/g, personFirstName);
|
|
92
|
+
}
|
|
93
|
+
/** Base system rules prompt */
|
|
94
|
+
get baseSystemRules() {
|
|
95
|
+
return this.load("base.txt");
|
|
96
|
+
}
|
|
97
|
+
/** Public persona prompt */
|
|
98
|
+
get publicPersona() {
|
|
99
|
+
return this.load("public.txt");
|
|
100
|
+
}
|
|
101
|
+
/** Private persona prompt */
|
|
102
|
+
get privatePersona() {
|
|
103
|
+
return this.load("private.txt");
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/core/retrieval.ts
|
|
108
|
+
var import_client = require("@libsql/client");
|
|
109
|
+
|
|
110
|
+
// src/core/loaders.ts
|
|
111
|
+
var import_node_fs2 = require("node:fs");
|
|
112
|
+
var import_node_path2 = require("node:path");
|
|
113
|
+
function walk(dir) {
|
|
114
|
+
const items = [];
|
|
115
|
+
try {
|
|
116
|
+
for (const e of (0, import_node_fs2.readdirSync)(dir)) {
|
|
117
|
+
const p = (0, import_node_path2.join)(dir, e);
|
|
118
|
+
const st = (0, import_node_fs2.statSync)(p);
|
|
119
|
+
if (st.isDirectory()) items.push(...walk(p));
|
|
120
|
+
else if (p.endsWith(".md")) items.push(p);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
function loadKnowledge(root = "config/knowledge") {
|
|
127
|
+
const docs = [];
|
|
128
|
+
for (const bucket of ["base", "public", "private"]) {
|
|
129
|
+
const dir = (0, import_node_path2.join)(root, bucket);
|
|
130
|
+
for (const f of walk(dir)) {
|
|
131
|
+
docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return docs;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/core/retrieval.ts
|
|
138
|
+
var EMB_MODEL = "text-embedding-3-large";
|
|
139
|
+
function chunk(text, max = 900) {
|
|
140
|
+
const out = [];
|
|
141
|
+
let buf = "";
|
|
142
|
+
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
143
|
+
if (`${buf}
|
|
144
|
+
${line}`.length > max) {
|
|
145
|
+
out.push(buf.trim());
|
|
146
|
+
buf = line;
|
|
147
|
+
} else buf += `
|
|
148
|
+
${line}`;
|
|
149
|
+
}
|
|
150
|
+
if (buf.trim()) out.push(buf.trim());
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
async function sha256(input) {
|
|
154
|
+
const data = new TextEncoder().encode(input);
|
|
155
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
156
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
157
|
+
}
|
|
158
|
+
var VectorStore = class _VectorStore {
|
|
159
|
+
constructor(client, options) {
|
|
160
|
+
this.client = client;
|
|
161
|
+
this.db = (0, import_client.createClient)({
|
|
162
|
+
url: options.databaseUrl,
|
|
163
|
+
authToken: options.databaseAuthToken
|
|
164
|
+
});
|
|
165
|
+
this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
|
|
166
|
+
}
|
|
167
|
+
db;
|
|
168
|
+
knowledgeDir;
|
|
169
|
+
// On boot: ingest new chunks and embed only missing ones.
|
|
170
|
+
async build() {
|
|
171
|
+
console.log("\u{1F504} Building knowledge base...");
|
|
172
|
+
await this.db.execute(`
|
|
173
|
+
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
174
|
+
`);
|
|
175
|
+
await this.db.execute(`
|
|
176
|
+
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
177
|
+
`);
|
|
178
|
+
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
179
|
+
const docs = loadKnowledge(this.knowledgeDir);
|
|
180
|
+
console.log(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
|
|
181
|
+
const rows = [];
|
|
182
|
+
for (const d of docs) {
|
|
183
|
+
for (const part of chunk(d.text)) {
|
|
184
|
+
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
185
|
+
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.log(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
|
|
189
|
+
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
190
|
+
const tx = await this.db.transaction("write");
|
|
191
|
+
try {
|
|
192
|
+
for (const r of rows) {
|
|
193
|
+
await tx.execute({
|
|
194
|
+
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
195
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
196
|
+
args: [r.id, r.bucket, r.source, r.text]
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
await tx.commit();
|
|
200
|
+
} catch (e) {
|
|
201
|
+
await tx.rollback();
|
|
202
|
+
throw e;
|
|
203
|
+
}
|
|
204
|
+
const ids = rows.map((r) => r.id);
|
|
205
|
+
const missing = [];
|
|
206
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
207
|
+
const batch = ids.slice(i, i + 500);
|
|
208
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
209
|
+
const res = await this.db.execute({
|
|
210
|
+
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
211
|
+
EXCEPT SELECT id FROM embeddings`,
|
|
212
|
+
args: batch
|
|
213
|
+
});
|
|
214
|
+
for (const row of res.rows) missing.push(String(row.id));
|
|
215
|
+
}
|
|
216
|
+
if (missing.length === 0) {
|
|
217
|
+
console.log("\u2705 No new chunks to embed - knowledge base is up to date");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
console.log(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
221
|
+
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
222
|
+
const BATCH = 96;
|
|
223
|
+
for (let i = 0; i < missing.length; i += BATCH) {
|
|
224
|
+
const batchIds = missing.slice(i, i + BATCH);
|
|
225
|
+
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
226
|
+
const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
|
|
227
|
+
const tx2 = await this.db.transaction("write");
|
|
228
|
+
try {
|
|
229
|
+
emb.data.forEach((d, idx) => {
|
|
230
|
+
const id = batchIds[idx];
|
|
231
|
+
tx2.execute({
|
|
232
|
+
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
233
|
+
args: [id, EMB_MODEL, JSON.stringify(d.embedding)]
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
await tx2.commit();
|
|
237
|
+
} catch (e) {
|
|
238
|
+
await tx2.rollback();
|
|
239
|
+
throw e;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
console.log(`\u2705 Successfully embedded ${missing.length} new chunks`);
|
|
243
|
+
}
|
|
244
|
+
// Remove chunks from database that no longer exist in markdown files
|
|
245
|
+
async cleanupStaleChunks(currentIds) {
|
|
246
|
+
const result = await this.db.execute("SELECT id FROM chunks");
|
|
247
|
+
const dbIds = result.rows.map((row) => String(row.id));
|
|
248
|
+
const currentIdSet = new Set(currentIds);
|
|
249
|
+
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
250
|
+
if (staleIds.length === 0) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
console.log(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
|
|
254
|
+
const BATCH_SIZE = 500;
|
|
255
|
+
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
256
|
+
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
257
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
258
|
+
await this.db.execute({
|
|
259
|
+
sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
|
|
260
|
+
args: batch
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
console.log(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
264
|
+
}
|
|
265
|
+
static cosine(a, b) {
|
|
266
|
+
let dot = 0;
|
|
267
|
+
let na = 0;
|
|
268
|
+
let nb = 0;
|
|
269
|
+
for (let i = 0; i < a.length; i++) {
|
|
270
|
+
dot += a[i] * b[i];
|
|
271
|
+
na += a[i] * a[i];
|
|
272
|
+
nb += b[i] * b[i];
|
|
273
|
+
}
|
|
274
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
275
|
+
}
|
|
276
|
+
// Retrieve top-k across allowed buckets; compute similarity in app (simple & portable).
|
|
277
|
+
async query(q, k = 6, allowed = ["base"]) {
|
|
278
|
+
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
279
|
+
const placeholders = allowed.map(() => "?").join(",");
|
|
280
|
+
const res = await this.db.execute({
|
|
281
|
+
sql: `SELECT c.id, c.text, e.embedding
|
|
282
|
+
FROM chunks c
|
|
283
|
+
JOIN embeddings e ON e.id = c.id
|
|
284
|
+
WHERE c.bucket IN (${placeholders})`,
|
|
285
|
+
args: allowed
|
|
286
|
+
});
|
|
287
|
+
const scored = [];
|
|
288
|
+
for (const row of res.rows) {
|
|
289
|
+
const emb = JSON.parse(String(row.embedding));
|
|
290
|
+
const s = _VectorStore.cosine(qv, emb);
|
|
291
|
+
scored.push({ s, text: String(row.text) });
|
|
292
|
+
}
|
|
293
|
+
scored.sort((a, b) => b.s - a.s);
|
|
294
|
+
return scored.slice(0, k).map((r) => r.text);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// src/mcp-server/conversation-id.ts
|
|
299
|
+
function generateConversationId() {
|
|
300
|
+
const timestamp = Date.now();
|
|
301
|
+
const random = Math.random().toString(36).slice(2, 11);
|
|
302
|
+
return `conv_${timestamp}_${random}`;
|
|
303
|
+
}
|
|
304
|
+
function getOrGenerateConversationId(providedId) {
|
|
305
|
+
return providedId || generateConversationId();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/mcp-server/cost-tracker.ts
|
|
309
|
+
function calculateCost(usage) {
|
|
310
|
+
const promptTokens = usage.prompt_tokens || 0;
|
|
311
|
+
const completionTokens = usage.completion_tokens || 0;
|
|
312
|
+
const totalTokens = promptTokens + completionTokens;
|
|
313
|
+
const promptCost = promptTokens / 1e6 * 2.5;
|
|
314
|
+
const completionCost = completionTokens / 1e6 * 10;
|
|
315
|
+
const estimatedCost = promptCost + completionCost;
|
|
316
|
+
return {
|
|
317
|
+
promptTokens,
|
|
318
|
+
completionTokens,
|
|
319
|
+
totalTokens,
|
|
320
|
+
estimatedCost
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/mcp-server/logger.ts
|
|
325
|
+
var MCPLogger = class {
|
|
326
|
+
enableConsole;
|
|
327
|
+
callback;
|
|
328
|
+
constructor(enableConsole = true, callback) {
|
|
329
|
+
this.enableConsole = enableConsole;
|
|
330
|
+
this.callback = callback;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Log a chat interaction
|
|
334
|
+
*/
|
|
335
|
+
async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
|
|
336
|
+
const lastUserMsg = [...conversationMessages].reverse().find((m) => m.role === "user");
|
|
337
|
+
const logEvent = {
|
|
338
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
339
|
+
toolName,
|
|
340
|
+
conversationId,
|
|
341
|
+
userMessage: lastUserMsg?.content || "",
|
|
342
|
+
conversationHistory: conversationMessages,
|
|
343
|
+
ragContext,
|
|
344
|
+
response,
|
|
345
|
+
duration,
|
|
346
|
+
cost
|
|
347
|
+
};
|
|
348
|
+
if (this.enableConsole) {
|
|
349
|
+
console.log(
|
|
350
|
+
JSON.stringify({
|
|
351
|
+
event: "mcp_chat",
|
|
352
|
+
...logEvent
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (this.callback) {
|
|
357
|
+
try {
|
|
358
|
+
await this.callback(logEvent);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
console.error("Logging callback error:", err);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
function createLogger(enableConsole = true, callback) {
|
|
366
|
+
return new MCPLogger(enableConsole, callback);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/mcp-server/rate-limiter.ts
|
|
370
|
+
var RateLimiter = class {
|
|
371
|
+
requestMap;
|
|
372
|
+
limit;
|
|
373
|
+
windowMs;
|
|
374
|
+
/**
|
|
375
|
+
* Create a new rate limiter
|
|
376
|
+
*
|
|
377
|
+
* @param limit - Maximum requests allowed in the time window
|
|
378
|
+
* @param windowMs - Time window in milliseconds (default: 60000 = 1 minute)
|
|
379
|
+
*/
|
|
380
|
+
constructor(limit, windowMs = 6e4) {
|
|
381
|
+
this.requestMap = /* @__PURE__ */ new Map();
|
|
382
|
+
this.limit = limit;
|
|
383
|
+
this.windowMs = windowMs;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Check if a request should be allowed
|
|
387
|
+
*
|
|
388
|
+
* @param key - Unique identifier for the rate limit (e.g., tool name)
|
|
389
|
+
* @returns true if request is allowed, false if rate limit exceeded
|
|
390
|
+
*/
|
|
391
|
+
check(key) {
|
|
392
|
+
const now = Date.now();
|
|
393
|
+
const windowStart = now - this.windowMs;
|
|
394
|
+
const timestamps = this.requestMap.get(key) || [];
|
|
395
|
+
const recentTimestamps = timestamps.filter((ts) => ts > windowStart);
|
|
396
|
+
if (recentTimestamps.length >= this.limit) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
recentTimestamps.push(now);
|
|
400
|
+
this.requestMap.set(key, recentTimestamps);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get current request count for a key
|
|
405
|
+
*
|
|
406
|
+
* @param key - Unique identifier
|
|
407
|
+
* @returns Current number of requests in the window
|
|
408
|
+
*/
|
|
409
|
+
getCount(key) {
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
const windowStart = now - this.windowMs;
|
|
412
|
+
const timestamps = this.requestMap.get(key) || [];
|
|
413
|
+
return timestamps.filter((ts) => ts > windowStart).length;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Reset rate limit for a specific key
|
|
417
|
+
*
|
|
418
|
+
* @param key - Unique identifier to reset
|
|
419
|
+
*/
|
|
420
|
+
reset(key) {
|
|
421
|
+
this.requestMap.delete(key);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Reset all rate limits
|
|
425
|
+
*/
|
|
426
|
+
resetAll() {
|
|
427
|
+
this.requestMap.clear();
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get time until rate limit resets (in ms)
|
|
431
|
+
*
|
|
432
|
+
* @param key - Unique identifier
|
|
433
|
+
* @returns Milliseconds until the oldest request falls out of the window
|
|
434
|
+
*/
|
|
435
|
+
getTimeUntilReset(key) {
|
|
436
|
+
const timestamps = this.requestMap.get(key) || [];
|
|
437
|
+
if (timestamps.length === 0) return 0;
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
const oldestTimestamp = Math.min(...timestamps);
|
|
440
|
+
const timeUntilReset = this.windowMs - (now - oldestTimestamp);
|
|
441
|
+
return Math.max(0, timeUntilReset);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
function createRateLimiter(limit, windowMs = 6e4) {
|
|
445
|
+
return limit ? new RateLimiter(limit, windowMs) : null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/mcp-server.ts
|
|
449
|
+
async function createMCPServer(config) {
|
|
450
|
+
console.log(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
|
|
451
|
+
const knowledgeDir = config.knowledgeDir || "./config/knowledge";
|
|
452
|
+
const promptsDir = config.promptsDir || "./config/prompts";
|
|
453
|
+
const transportMode = config.transport || "stdio";
|
|
454
|
+
const rateLimiter = createRateLimiter(config.toolRateLimit);
|
|
455
|
+
const logger = createLogger(config.logging?.console !== false, config.logging?.onChat);
|
|
456
|
+
const publicToolConfig = {
|
|
457
|
+
enabled: config.tools?.public?.enabled !== false,
|
|
458
|
+
name: config.tools?.public?.name || "chat_public",
|
|
459
|
+
description: config.tools?.public?.description || `Chat with ${config.bot.name} using public knowledge base. Supports both single messages and conversation history.`
|
|
460
|
+
};
|
|
461
|
+
const privateToolConfig = {
|
|
462
|
+
enabled: config.tools?.private?.enabled !== false,
|
|
463
|
+
name: config.tools?.private?.name || "chat_private",
|
|
464
|
+
description: config.tools?.private?.description || `Chat with ${config.bot.name} using private/internal knowledge base. Supports both single messages and conversation history.`
|
|
465
|
+
};
|
|
466
|
+
const client = new import_openai.default({
|
|
467
|
+
apiKey: config.openai.apiKey
|
|
468
|
+
});
|
|
469
|
+
const store = new VectorStore(client, {
|
|
470
|
+
databaseUrl: config.database.url,
|
|
471
|
+
databaseAuthToken: config.database.authToken,
|
|
472
|
+
knowledgeDir
|
|
473
|
+
});
|
|
474
|
+
await store.build();
|
|
475
|
+
console.log("\u2705 Knowledge base ready");
|
|
476
|
+
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
477
|
+
const server = new import_mcp.McpServer({
|
|
478
|
+
name: config.bot.name,
|
|
479
|
+
version: "1.0.0"
|
|
480
|
+
});
|
|
481
|
+
console.log("\u2705 MCP server initialized");
|
|
482
|
+
if (publicToolConfig.enabled) {
|
|
483
|
+
const publicSchema = import_zod.z.object({
|
|
484
|
+
message: import_zod.z.string().optional().describe("A single message to send (for simple queries)"),
|
|
485
|
+
messages: import_zod.z.array(
|
|
486
|
+
import_zod.z.object({
|
|
487
|
+
role: import_zod.z.enum(["user", "assistant"]),
|
|
488
|
+
content: import_zod.z.string()
|
|
489
|
+
})
|
|
490
|
+
).optional().describe("Conversation history with alternating user/assistant messages"),
|
|
491
|
+
conversationId: import_zod.z.string().optional().describe(
|
|
492
|
+
"Optional conversation ID to track sessions. If not provided, a new ID will be generated."
|
|
493
|
+
)
|
|
494
|
+
}).refine(
|
|
495
|
+
(data) => data.message !== void 0 || data.messages !== void 0,
|
|
496
|
+
"Either 'message' or 'messages' must be provided"
|
|
497
|
+
);
|
|
498
|
+
server.registerTool(
|
|
499
|
+
publicToolConfig.name,
|
|
500
|
+
{
|
|
501
|
+
description: publicToolConfig.description,
|
|
502
|
+
inputSchema: publicSchema
|
|
503
|
+
},
|
|
504
|
+
async (args) => {
|
|
505
|
+
const { message, messages, conversationId } = args;
|
|
506
|
+
const startTime = Date.now();
|
|
507
|
+
const convId = getOrGenerateConversationId(conversationId);
|
|
508
|
+
if (rateLimiter && !rateLimiter.check(publicToolConfig.name)) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Rate limit exceeded for ${publicToolConfig.name}. Maximum ${config.toolRateLimit} requests per minute.`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
let conversationMessages;
|
|
514
|
+
if (messages && Array.isArray(messages)) {
|
|
515
|
+
conversationMessages = messages;
|
|
516
|
+
} else if (message) {
|
|
517
|
+
conversationMessages = [{ role: "user", content: String(message) }];
|
|
518
|
+
} else {
|
|
519
|
+
throw new Error("Either 'message' or 'messages' is required");
|
|
520
|
+
}
|
|
521
|
+
const lastUserMsg = [...conversationMessages].reverse().find((m) => m.role === "user");
|
|
522
|
+
if (!lastUserMsg) {
|
|
523
|
+
throw new Error("No user message found in conversation");
|
|
524
|
+
}
|
|
525
|
+
const ctx = await store.query(lastUserMsg.content, 6, ["base", "public"]);
|
|
526
|
+
const system = [
|
|
527
|
+
prompts.baseSystemRules,
|
|
528
|
+
prompts.publicPersona,
|
|
529
|
+
`Context:
|
|
530
|
+
${ctx.join("\n\n")}`
|
|
531
|
+
].join("\n\n");
|
|
532
|
+
const result = await completeOnce({
|
|
533
|
+
client,
|
|
534
|
+
system,
|
|
535
|
+
messages: conversationMessages
|
|
536
|
+
});
|
|
537
|
+
const cost = calculateCost(result.usage);
|
|
538
|
+
const duration = Date.now() - startTime;
|
|
539
|
+
await logger.logChatInteraction(
|
|
540
|
+
publicToolConfig.name,
|
|
541
|
+
convId,
|
|
542
|
+
conversationMessages,
|
|
543
|
+
ctx,
|
|
544
|
+
result.content,
|
|
545
|
+
duration,
|
|
546
|
+
cost
|
|
547
|
+
).catch((err) => console.error("Logging error:", err));
|
|
548
|
+
return {
|
|
549
|
+
content: [
|
|
550
|
+
{
|
|
551
|
+
type: "text",
|
|
552
|
+
text: result.content
|
|
553
|
+
}
|
|
554
|
+
],
|
|
555
|
+
_meta: {
|
|
556
|
+
conversationId: convId,
|
|
557
|
+
cost: {
|
|
558
|
+
promptTokens: cost.promptTokens,
|
|
559
|
+
completionTokens: cost.completionTokens,
|
|
560
|
+
totalTokens: cost.totalTokens,
|
|
561
|
+
estimatedCostUSD: cost.estimatedCost
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
console.log(`\u2705 Registered tool: ${publicToolConfig.name}`);
|
|
568
|
+
}
|
|
569
|
+
if (privateToolConfig.enabled) {
|
|
570
|
+
const privateSchema = import_zod.z.object({
|
|
571
|
+
message: import_zod.z.string().optional().describe("A single message to send (for simple queries)"),
|
|
572
|
+
messages: import_zod.z.array(
|
|
573
|
+
import_zod.z.object({
|
|
574
|
+
role: import_zod.z.enum(["user", "assistant"]),
|
|
575
|
+
content: import_zod.z.string()
|
|
576
|
+
})
|
|
577
|
+
).optional().describe("Conversation history with alternating user/assistant messages"),
|
|
578
|
+
conversationId: import_zod.z.string().optional().describe(
|
|
579
|
+
"Optional conversation ID to track sessions. If not provided, a new ID will be generated."
|
|
580
|
+
)
|
|
581
|
+
}).refine(
|
|
582
|
+
(data) => data.message !== void 0 || data.messages !== void 0,
|
|
583
|
+
"Either 'message' or 'messages' must be provided"
|
|
584
|
+
);
|
|
585
|
+
server.registerTool(
|
|
586
|
+
privateToolConfig.name,
|
|
587
|
+
{
|
|
588
|
+
description: privateToolConfig.description,
|
|
589
|
+
inputSchema: privateSchema
|
|
590
|
+
},
|
|
591
|
+
async (args) => {
|
|
592
|
+
const { message, messages, conversationId } = args;
|
|
593
|
+
const startTime = Date.now();
|
|
594
|
+
const convId = getOrGenerateConversationId(conversationId);
|
|
595
|
+
if (rateLimiter && !rateLimiter.check(privateToolConfig.name)) {
|
|
596
|
+
throw new Error(
|
|
597
|
+
`Rate limit exceeded for ${privateToolConfig.name}. Maximum ${config.toolRateLimit} requests per minute.`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
let conversationMessages;
|
|
601
|
+
if (messages && Array.isArray(messages)) {
|
|
602
|
+
conversationMessages = messages;
|
|
603
|
+
} else if (message) {
|
|
604
|
+
conversationMessages = [{ role: "user", content: String(message) }];
|
|
605
|
+
} else {
|
|
606
|
+
throw new Error("Either 'message' or 'messages' is required");
|
|
607
|
+
}
|
|
608
|
+
const lastUserMsg = [...conversationMessages].reverse().find((m) => m.role === "user");
|
|
609
|
+
if (!lastUserMsg) {
|
|
610
|
+
throw new Error("No user message found in conversation");
|
|
611
|
+
}
|
|
612
|
+
const ctx = await store.query(lastUserMsg.content, 8, ["base", "private"]);
|
|
613
|
+
const system = [
|
|
614
|
+
prompts.baseSystemRules,
|
|
615
|
+
prompts.privatePersona,
|
|
616
|
+
`Internal Context:
|
|
617
|
+
${ctx.join("\n\n")}`
|
|
618
|
+
].join("\n\n");
|
|
619
|
+
const result = await completeOnce({
|
|
620
|
+
client,
|
|
621
|
+
system,
|
|
622
|
+
messages: conversationMessages
|
|
623
|
+
});
|
|
624
|
+
const cost = calculateCost(result.usage);
|
|
625
|
+
const duration = Date.now() - startTime;
|
|
626
|
+
await logger.logChatInteraction(
|
|
627
|
+
privateToolConfig.name,
|
|
628
|
+
convId,
|
|
629
|
+
conversationMessages,
|
|
630
|
+
ctx,
|
|
631
|
+
result.content,
|
|
632
|
+
duration,
|
|
633
|
+
cost
|
|
634
|
+
).catch((err) => console.error("Logging error:", err));
|
|
635
|
+
return {
|
|
636
|
+
content: [
|
|
637
|
+
{
|
|
638
|
+
type: "text",
|
|
639
|
+
text: result.content
|
|
640
|
+
}
|
|
641
|
+
],
|
|
642
|
+
_meta: {
|
|
643
|
+
conversationId: convId,
|
|
644
|
+
cost: {
|
|
645
|
+
promptTokens: cost.promptTokens,
|
|
646
|
+
completionTokens: cost.completionTokens,
|
|
647
|
+
totalTokens: cost.totalTokens,
|
|
648
|
+
estimatedCostUSD: cost.estimatedCost
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
);
|
|
654
|
+
console.log(`\u2705 Registered tool: ${privateToolConfig.name}`);
|
|
655
|
+
}
|
|
656
|
+
const enabledTools = [
|
|
657
|
+
publicToolConfig.enabled ? publicToolConfig.name : null,
|
|
658
|
+
privateToolConfig.enabled ? privateToolConfig.name : null
|
|
659
|
+
].filter(Boolean);
|
|
660
|
+
if (enabledTools.length === 0) {
|
|
661
|
+
console.warn("\u26A0\uFE0F Warning: No MCP tools enabled");
|
|
662
|
+
}
|
|
663
|
+
console.log(
|
|
664
|
+
`\u2705 ${config.bot.name} MCP Server ready (transport: ${transportMode}, tools: ${enabledTools.join(", ") || "none"})`
|
|
665
|
+
);
|
|
666
|
+
return server;
|
|
667
|
+
}
|
|
668
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
669
|
+
0 && (module.exports = {
|
|
670
|
+
createMCPServer
|
|
671
|
+
});
|