@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 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 runLlmCompletion(llm, {
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
  };