@harborclient/team-hub 0.2.4 → 0.3.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/dist/cli.js +301 -3
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,35 @@ import path from "path";
|
|
|
25
25
|
import { parse as parseYaml } from "yaml";
|
|
26
26
|
|
|
27
27
|
// src/config/llmConfig.ts
|
|
28
|
+
function headerRowsFromSingleKeyObject(record) {
|
|
29
|
+
const rows = [];
|
|
30
|
+
for (const [key, value] of Object.entries(record)) {
|
|
31
|
+
const trimmedKey = key.trim();
|
|
32
|
+
if (trimmedKey.length > 0) {
|
|
33
|
+
rows.push({ key: trimmedKey, value: String(value) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
function normalizeHubMcpHeaders(headers) {
|
|
39
|
+
if (!headers) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(headers)) {
|
|
43
|
+
return headerRowsFromSingleKeyObject(headers);
|
|
44
|
+
}
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const item of headers) {
|
|
47
|
+
if (Array.isArray(item)) {
|
|
48
|
+
for (const nested of item) {
|
|
49
|
+
rows.push(...headerRowsFromSingleKeyObject(nested));
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
rows.push(...headerRowsFromSingleKeyObject(item));
|
|
54
|
+
}
|
|
55
|
+
return rows;
|
|
56
|
+
}
|
|
28
57
|
function normalizeLlmConfig(section) {
|
|
29
58
|
const providers = {};
|
|
30
59
|
if (section.providers.openai?.apiKey) {
|
|
@@ -36,9 +65,15 @@ function normalizeLlmConfig(section) {
|
|
|
36
65
|
if (section.providers.gemini?.apiKey) {
|
|
37
66
|
providers.gemini = { apiKey: section.providers.gemini.apiKey };
|
|
38
67
|
}
|
|
68
|
+
const mcp = section.mcp && section.mcp.length > 0 ? section.mcp.map((entry) => ({
|
|
69
|
+
name: entry.name.trim(),
|
|
70
|
+
url: entry.url.trim().replace(/\/+$/, ""),
|
|
71
|
+
headers: normalizeHubMcpHeaders(entry.headers)
|
|
72
|
+
})) : void 0;
|
|
39
73
|
return {
|
|
40
74
|
providers,
|
|
41
|
-
...section.models && section.models.length > 0 ? { models: section.models } : {}
|
|
75
|
+
...section.models && section.models.length > 0 ? { models: section.models } : {},
|
|
76
|
+
...mcp && mcp.length > 0 ? { mcp } : {}
|
|
42
77
|
};
|
|
43
78
|
}
|
|
44
79
|
|
|
@@ -116,6 +151,15 @@ var redisSectionSchema = z.object({
|
|
|
116
151
|
var llmProviderEntrySchema = z.object({
|
|
117
152
|
apiKey: z.string().trim().min(1, { message: "LLM provider apiKey must not be empty." })
|
|
118
153
|
});
|
|
154
|
+
var hubMcpHeadersSchema = z.union([
|
|
155
|
+
z.record(z.string(), z.string()),
|
|
156
|
+
z.array(z.union([z.record(z.string(), z.string()), z.array(z.record(z.string(), z.string()))]))
|
|
157
|
+
]);
|
|
158
|
+
var hubMcpServerEntrySchema = z.object({
|
|
159
|
+
name: z.string().trim().min(1),
|
|
160
|
+
url: z.string().trim().min(1),
|
|
161
|
+
headers: hubMcpHeadersSchema.optional()
|
|
162
|
+
});
|
|
119
163
|
var llmSectionSchema = z.object({
|
|
120
164
|
providers: z.object({
|
|
121
165
|
openai: llmProviderEntrySchema.optional(),
|
|
@@ -125,7 +169,8 @@ var llmSectionSchema = z.object({
|
|
|
125
169
|
(providers) => Boolean(providers.openai?.apiKey || providers.claude?.apiKey || providers.gemini?.apiKey),
|
|
126
170
|
{ message: "llm.providers must include at least one provider with an apiKey." }
|
|
127
171
|
),
|
|
128
|
-
models: z.array(z.string().trim().min(1)).optional()
|
|
172
|
+
models: z.array(z.string().trim().min(1)).optional(),
|
|
173
|
+
mcp: z.array(hubMcpServerEntrySchema).optional()
|
|
129
174
|
});
|
|
130
175
|
var pluginsSectionSchema = z.object({
|
|
131
176
|
catalogs: z.array(z.string().trim().url()).optional(),
|
|
@@ -8216,6 +8261,258 @@ async function runLlmCompletion(config, input) {
|
|
|
8216
8261
|
return parseCompletionResponse(json);
|
|
8217
8262
|
}
|
|
8218
8263
|
|
|
8264
|
+
// src/server/llm/hubMcpToolNames.ts
|
|
8265
|
+
var HUB_MCP_TOOL_PREFIX = "hubmcp__";
|
|
8266
|
+
function encodeHubMcpToolName(serverIndex, toolName) {
|
|
8267
|
+
return `${HUB_MCP_TOOL_PREFIX}${serverIndex}__${toolName}`;
|
|
8268
|
+
}
|
|
8269
|
+
function decodeHubMcpToolName(prefixed) {
|
|
8270
|
+
if (!prefixed.startsWith(HUB_MCP_TOOL_PREFIX)) {
|
|
8271
|
+
return null;
|
|
8272
|
+
}
|
|
8273
|
+
const rest = prefixed.slice(HUB_MCP_TOOL_PREFIX.length);
|
|
8274
|
+
const separatorIndex = rest.indexOf("__");
|
|
8275
|
+
if (separatorIndex <= 0) {
|
|
8276
|
+
return null;
|
|
8277
|
+
}
|
|
8278
|
+
const serverIndex = Number.parseInt(rest.slice(0, separatorIndex), 10);
|
|
8279
|
+
if (!Number.isInteger(serverIndex) || serverIndex < 0) {
|
|
8280
|
+
return null;
|
|
8281
|
+
}
|
|
8282
|
+
const toolName = rest.slice(separatorIndex + 2);
|
|
8283
|
+
if (!toolName) {
|
|
8284
|
+
return null;
|
|
8285
|
+
}
|
|
8286
|
+
return { serverIndex, toolName };
|
|
8287
|
+
}
|
|
8288
|
+
function isHubMcpToolName(name) {
|
|
8289
|
+
return decodeHubMcpToolName(name) !== null;
|
|
8290
|
+
}
|
|
8291
|
+
|
|
8292
|
+
// src/server/llm/mcpClient.ts
|
|
8293
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8294
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
8295
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8296
|
+
var connectedClients = /* @__PURE__ */ new Map();
|
|
8297
|
+
var activeConfigSignature = "";
|
|
8298
|
+
function buildRequestHeaders(headers) {
|
|
8299
|
+
const result = {};
|
|
8300
|
+
for (const row of headers) {
|
|
8301
|
+
if (row.key.trim()) {
|
|
8302
|
+
result[row.key.trim()] = row.value;
|
|
8303
|
+
}
|
|
8304
|
+
}
|
|
8305
|
+
return result;
|
|
8306
|
+
}
|
|
8307
|
+
function buildMcpConfigSignature(config) {
|
|
8308
|
+
return JSON.stringify(config.mcp ?? []);
|
|
8309
|
+
}
|
|
8310
|
+
function flattenMcpToolContent(content) {
|
|
8311
|
+
if (!Array.isArray(content)) {
|
|
8312
|
+
return JSON.stringify(content ?? null);
|
|
8313
|
+
}
|
|
8314
|
+
const parts = content.map((item) => {
|
|
8315
|
+
if (!item || typeof item !== "object") {
|
|
8316
|
+
return String(item);
|
|
8317
|
+
}
|
|
8318
|
+
const record = item;
|
|
8319
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
8320
|
+
return record.text;
|
|
8321
|
+
}
|
|
8322
|
+
if (record.type === "resource" && typeof record.uri === "string") {
|
|
8323
|
+
return record.uri;
|
|
8324
|
+
}
|
|
8325
|
+
return JSON.stringify(record);
|
|
8326
|
+
});
|
|
8327
|
+
return parts.join("\n");
|
|
8328
|
+
}
|
|
8329
|
+
function toOpenAiTool(serverIndex, tool) {
|
|
8330
|
+
return {
|
|
8331
|
+
type: "function",
|
|
8332
|
+
function: {
|
|
8333
|
+
name: encodeHubMcpToolName(serverIndex, tool.name),
|
|
8334
|
+
description: tool.description ?? `MCP tool ${tool.name}`,
|
|
8335
|
+
parameters: tool.inputSchema ?? {
|
|
8336
|
+
type: "object",
|
|
8337
|
+
properties: {},
|
|
8338
|
+
additionalProperties: true
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
};
|
|
8342
|
+
}
|
|
8343
|
+
async function connectHubMcpServer(serverIndex, server) {
|
|
8344
|
+
const headers = buildRequestHeaders(server.headers);
|
|
8345
|
+
const url = new URL(server.url);
|
|
8346
|
+
const client = new Client(
|
|
8347
|
+
{
|
|
8348
|
+
name: "team-hub",
|
|
8349
|
+
version: "1.0.0"
|
|
8350
|
+
},
|
|
8351
|
+
{}
|
|
8352
|
+
);
|
|
8353
|
+
let transport;
|
|
8354
|
+
try {
|
|
8355
|
+
transport = new StreamableHTTPClientTransport(url, {
|
|
8356
|
+
requestInit: { headers }
|
|
8357
|
+
});
|
|
8358
|
+
await client.connect(transport);
|
|
8359
|
+
} catch {
|
|
8360
|
+
transport = new SSEClientTransport(url, {
|
|
8361
|
+
requestInit: { headers }
|
|
8362
|
+
});
|
|
8363
|
+
await client.connect(transport);
|
|
8364
|
+
}
|
|
8365
|
+
const { tools } = await client.listTools();
|
|
8366
|
+
return {
|
|
8367
|
+
serverIndex,
|
|
8368
|
+
client,
|
|
8369
|
+
remoteTools: tools
|
|
8370
|
+
};
|
|
8371
|
+
}
|
|
8372
|
+
async function ensureHubMcpConnections(config) {
|
|
8373
|
+
const servers = config.mcp ?? [];
|
|
8374
|
+
const signature = buildMcpConfigSignature(config);
|
|
8375
|
+
if (signature !== activeConfigSignature) {
|
|
8376
|
+
await disposeHubMcpConnections();
|
|
8377
|
+
activeConfigSignature = signature;
|
|
8378
|
+
}
|
|
8379
|
+
if (servers.length === 0) {
|
|
8380
|
+
return;
|
|
8381
|
+
}
|
|
8382
|
+
for (const [index, server] of servers.entries()) {
|
|
8383
|
+
if (connectedClients.has(index)) {
|
|
8384
|
+
continue;
|
|
8385
|
+
}
|
|
8386
|
+
try {
|
|
8387
|
+
const connected = await connectHubMcpServer(index, server);
|
|
8388
|
+
connectedClients.set(index, connected);
|
|
8389
|
+
} catch (error) {
|
|
8390
|
+
const message = error instanceof Error ? error.message : "Connection failed";
|
|
8391
|
+
console.warn(`Hub MCP server "${server.name}" failed to connect: ${message}`);
|
|
8392
|
+
}
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
function listHubMcpTools() {
|
|
8396
|
+
const tools = [];
|
|
8397
|
+
for (const entry of connectedClients.values()) {
|
|
8398
|
+
for (const tool of entry.remoteTools) {
|
|
8399
|
+
tools.push(toOpenAiTool(entry.serverIndex, tool));
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
return tools;
|
|
8403
|
+
}
|
|
8404
|
+
async function callHubMcpTool(prefixedName, args) {
|
|
8405
|
+
const decoded = decodeHubMcpToolName(prefixedName);
|
|
8406
|
+
if (!decoded) {
|
|
8407
|
+
return JSON.stringify({ error: `Unknown hub MCP tool: ${prefixedName}` });
|
|
8408
|
+
}
|
|
8409
|
+
const entry = connectedClients.get(decoded.serverIndex);
|
|
8410
|
+
if (!entry) {
|
|
8411
|
+
return JSON.stringify({ error: `Hub MCP server ${decoded.serverIndex} is not connected.` });
|
|
8412
|
+
}
|
|
8413
|
+
try {
|
|
8414
|
+
const result = await entry.client.callTool({
|
|
8415
|
+
name: decoded.toolName,
|
|
8416
|
+
arguments: args ?? {}
|
|
8417
|
+
});
|
|
8418
|
+
if (result.isError) {
|
|
8419
|
+
return JSON.stringify({
|
|
8420
|
+
error: flattenMcpToolContent(result.content)
|
|
8421
|
+
});
|
|
8422
|
+
}
|
|
8423
|
+
return flattenMcpToolContent(result.content);
|
|
8424
|
+
} catch (error) {
|
|
8425
|
+
const message = error instanceof Error ? error.message : "MCP tool execution failed.";
|
|
8426
|
+
return JSON.stringify({ error: message });
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
async function disposeHubMcpConnections() {
|
|
8430
|
+
const closePromises = [...connectedClients.values()].map(async (entry) => {
|
|
8431
|
+
try {
|
|
8432
|
+
await entry.client.close();
|
|
8433
|
+
} catch {
|
|
8434
|
+
}
|
|
8435
|
+
});
|
|
8436
|
+
connectedClients.clear();
|
|
8437
|
+
activeConfigSignature = "";
|
|
8438
|
+
await Promise.allSettled(closePromises);
|
|
8439
|
+
}
|
|
8440
|
+
|
|
8441
|
+
// src/server/llm/agent.ts
|
|
8442
|
+
var HUB_CHAT_STEP_MAX_ITERATIONS = 8;
|
|
8443
|
+
function addUsage(current, next) {
|
|
8444
|
+
return {
|
|
8445
|
+
promptTokens: current.promptTokens + next.promptTokens,
|
|
8446
|
+
completionTokens: current.completionTokens + next.completionTokens,
|
|
8447
|
+
totalTokens: current.totalTokens + next.totalTokens
|
|
8448
|
+
};
|
|
8449
|
+
}
|
|
8450
|
+
function parseToolArguments(raw) {
|
|
8451
|
+
try {
|
|
8452
|
+
return JSON.parse(raw);
|
|
8453
|
+
} catch {
|
|
8454
|
+
return {};
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
async function runHubChatStep(config, input, deps = {}) {
|
|
8458
|
+
const runCompletion = deps.runCompletion ?? runLlmCompletion;
|
|
8459
|
+
const ensureConnections = deps.ensureConnections ?? ensureHubMcpConnections;
|
|
8460
|
+
const listTools = deps.listTools ?? listHubMcpTools;
|
|
8461
|
+
const callTool = deps.callTool ?? callHubMcpTool;
|
|
8462
|
+
await ensureConnections(config);
|
|
8463
|
+
const hubTools = listTools();
|
|
8464
|
+
const mergedTools = hubTools.length > 0 || (input.tools?.length ?? 0) > 0 ? [...hubTools, ...input.tools ?? []] : void 0;
|
|
8465
|
+
let messages = [...input.messages];
|
|
8466
|
+
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
8467
|
+
let lastContent = null;
|
|
8468
|
+
for (let iteration = 0; iteration < HUB_CHAT_STEP_MAX_ITERATIONS; iteration += 1) {
|
|
8469
|
+
const result = await runCompletion(config, {
|
|
8470
|
+
model: input.model,
|
|
8471
|
+
messages,
|
|
8472
|
+
systemPrompt: input.systemPrompt,
|
|
8473
|
+
tools: mergedTools
|
|
8474
|
+
});
|
|
8475
|
+
usage = addUsage(usage, result.usage);
|
|
8476
|
+
lastContent = result.content;
|
|
8477
|
+
const toolCalls = result.toolCalls ?? [];
|
|
8478
|
+
if (toolCalls.length === 0) {
|
|
8479
|
+
return {
|
|
8480
|
+
content: result.content,
|
|
8481
|
+
usage
|
|
8482
|
+
};
|
|
8483
|
+
}
|
|
8484
|
+
const hubCalls = toolCalls.filter((call) => isHubMcpToolName(call.name));
|
|
8485
|
+
const passthroughCalls = toolCalls.filter((call) => !isHubMcpToolName(call.name));
|
|
8486
|
+
if (passthroughCalls.length > 0) {
|
|
8487
|
+
return {
|
|
8488
|
+
content: result.content,
|
|
8489
|
+
toolCalls: passthroughCalls,
|
|
8490
|
+
usage
|
|
8491
|
+
};
|
|
8492
|
+
}
|
|
8493
|
+
messages = [
|
|
8494
|
+
...messages,
|
|
8495
|
+
{
|
|
8496
|
+
role: "assistant",
|
|
8497
|
+
content: result.content,
|
|
8498
|
+
tool_calls: hubCalls
|
|
8499
|
+
}
|
|
8500
|
+
];
|
|
8501
|
+
for (const call of hubCalls) {
|
|
8502
|
+
const toolResult = await callTool(call.name, parseToolArguments(call.arguments));
|
|
8503
|
+
messages.push({
|
|
8504
|
+
role: "tool",
|
|
8505
|
+
tool_call_id: call.id,
|
|
8506
|
+
content: toolResult
|
|
8507
|
+
});
|
|
8508
|
+
}
|
|
8509
|
+
}
|
|
8510
|
+
return {
|
|
8511
|
+
content: lastContent,
|
|
8512
|
+
usage
|
|
8513
|
+
};
|
|
8514
|
+
}
|
|
8515
|
+
|
|
8219
8516
|
// src/server/routes/llm.ts
|
|
8220
8517
|
function sendMonthlyLimitExceeded(reply) {
|
|
8221
8518
|
return reply.code(402).send({
|
|
@@ -8333,7 +8630,7 @@ async function registerLlmRoutes(app, options) {
|
|
|
8333
8630
|
if (isNewTurn && isOverMonthlyLimit(totalTokens, user.llmMonthlyTokenLimit)) {
|
|
8334
8631
|
return sendMonthlyLimitExceeded(reply);
|
|
8335
8632
|
}
|
|
8336
|
-
const result = await
|
|
8633
|
+
const result = await runHubChatStep(llm, {
|
|
8337
8634
|
model,
|
|
8338
8635
|
messages,
|
|
8339
8636
|
tools,
|
|
@@ -8825,6 +9122,7 @@ function registerGracefulShutdown(app, ctx) {
|
|
|
8825
9122
|
const shutdown = async (signal) => {
|
|
8826
9123
|
app.log.info(`Received ${signal}, shutting down.`);
|
|
8827
9124
|
await app.close();
|
|
9125
|
+
await disposeHubMcpConnections();
|
|
8828
9126
|
await disconnectAll(ctx);
|
|
8829
9127
|
process.exit(0);
|
|
8830
9128
|
};
|