@hubium/hubium-mcp 0.1.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.
Files changed (90) hide show
  1. package/README.md +15 -0
  2. package/dist/cli.d.ts +22 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +270 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cli.test.d.ts +2 -0
  7. package/dist/cli.test.d.ts.map +1 -0
  8. package/dist/cli.test.js +191 -0
  9. package/dist/cli.test.js.map +1 -0
  10. package/dist/context/contextManager.d.ts +24 -0
  11. package/dist/context/contextManager.d.ts.map +1 -0
  12. package/dist/context/contextManager.js +129 -0
  13. package/dist/context/contextManager.js.map +1 -0
  14. package/dist/context/contextManager.test.d.ts +2 -0
  15. package/dist/context/contextManager.test.d.ts.map +1 -0
  16. package/dist/context/contextManager.test.js +101 -0
  17. package/dist/context/contextManager.test.js.map +1 -0
  18. package/dist/context/contextStore.d.ts +11 -0
  19. package/dist/context/contextStore.d.ts.map +1 -0
  20. package/dist/context/contextStore.js +116 -0
  21. package/dist/context/contextStore.js.map +1 -0
  22. package/dist/context/index.d.ts +5 -0
  23. package/dist/context/index.d.ts.map +1 -0
  24. package/dist/context/index.js +4 -0
  25. package/dist/context/index.js.map +1 -0
  26. package/dist/context/paths.d.ts +3 -0
  27. package/dist/context/paths.d.ts.map +1 -0
  28. package/dist/context/paths.js +23 -0
  29. package/dist/context/paths.js.map +1 -0
  30. package/dist/context/types.d.ts +9 -0
  31. package/dist/context/types.d.ts.map +1 -0
  32. package/dist/context/types.js +2 -0
  33. package/dist/context/types.js.map +1 -0
  34. package/dist/doctor/doctor.d.ts +84 -0
  35. package/dist/doctor/doctor.d.ts.map +1 -0
  36. package/dist/doctor/doctor.js +338 -0
  37. package/dist/doctor/doctor.js.map +1 -0
  38. package/dist/doctor/doctor.test.d.ts +5 -0
  39. package/dist/doctor/doctor.test.d.ts.map +1 -0
  40. package/dist/doctor/doctor.test.js +264 -0
  41. package/dist/doctor/doctor.test.js.map +1 -0
  42. package/dist/install/cursorInstaller.d.ts +15 -0
  43. package/dist/install/cursorInstaller.d.ts.map +1 -0
  44. package/dist/install/cursorInstaller.js +110 -0
  45. package/dist/install/cursorInstaller.js.map +1 -0
  46. package/dist/install/cursorInstaller.test.d.ts +2 -0
  47. package/dist/install/cursorInstaller.test.d.ts.map +1 -0
  48. package/dist/install/cursorInstaller.test.js +120 -0
  49. package/dist/install/cursorInstaller.test.js.map +1 -0
  50. package/dist/server.d.ts +72 -0
  51. package/dist/server.d.ts.map +1 -0
  52. package/dist/server.js +809 -0
  53. package/dist/server.js.map +1 -0
  54. package/dist/server.test.d.ts +2 -0
  55. package/dist/server.test.d.ts.map +1 -0
  56. package/dist/server.test.js +9 -0
  57. package/dist/server.test.js.map +1 -0
  58. package/dist/server.tools.test.d.ts +2 -0
  59. package/dist/server.tools.test.d.ts.map +1 -0
  60. package/dist/server.tools.test.js +991 -0
  61. package/dist/server.tools.test.js.map +1 -0
  62. package/dist/token/index.d.ts +5 -0
  63. package/dist/token/index.d.ts.map +1 -0
  64. package/dist/token/index.js +4 -0
  65. package/dist/token/index.js.map +1 -0
  66. package/dist/token/keychain.d.ts +4 -0
  67. package/dist/token/keychain.d.ts.map +1 -0
  68. package/dist/token/keychain.js +11 -0
  69. package/dist/token/keychain.js.map +1 -0
  70. package/dist/token/redactor.d.ts +8 -0
  71. package/dist/token/redactor.d.ts.map +1 -0
  72. package/dist/token/redactor.js +88 -0
  73. package/dist/token/redactor.js.map +1 -0
  74. package/dist/token/tokenStore.d.ts +17 -0
  75. package/dist/token/tokenStore.d.ts.map +1 -0
  76. package/dist/token/tokenStore.js +98 -0
  77. package/dist/token/tokenStore.js.map +1 -0
  78. package/dist/token/tokenStore.test.d.ts +2 -0
  79. package/dist/token/tokenStore.test.d.ts.map +1 -0
  80. package/dist/token/tokenStore.test.js +89 -0
  81. package/dist/token/tokenStore.test.js.map +1 -0
  82. package/dist/trustBanner.d.ts +7 -0
  83. package/dist/trustBanner.d.ts.map +1 -0
  84. package/dist/trustBanner.js +14 -0
  85. package/dist/trustBanner.js.map +1 -0
  86. package/dist/trustBanner.test.d.ts +2 -0
  87. package/dist/trustBanner.test.d.ts.map +1 -0
  88. package/dist/trustBanner.test.js +12 -0
  89. package/dist/trustBanner.test.js.map +1 -0
  90. package/package.json +27 -0
package/dist/server.js ADDED
@@ -0,0 +1,809 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { ContextManager } from './context/contextManager.js';
5
+ import { TokenStore } from './token/tokenStore.js';
6
+ import { runDoctor } from './doctor/doctor.js';
7
+ import { HubiumClient, HubiumClientError } from '@hubium/client';
8
+ import crypto from 'crypto';
9
+ import { Redactor } from './token/redactor.js';
10
+ export function createServer(deps) {
11
+ const contextManager = deps?.contextManager ?? new ContextManager();
12
+ const tokenStore = deps?.tokenStore ?? new TokenStore();
13
+ const hubiumClientFactory = deps?.hubiumClientFactory ?? ((input) => new HubiumClient({
14
+ baseUrl: input.baseUrl,
15
+ token: input.token,
16
+ userAgent: 'hubium-mcp/0.0.0',
17
+ maxRetries: 0,
18
+ }));
19
+ const safeMode = deps?.safeMode ?? getSafeMode();
20
+ const rateLimiter = deps?.rateLimiter ?? createRateLimiterFromEnv();
21
+ const server = new Server({
22
+ name: 'hubium-mcp',
23
+ version: '0.0.0',
24
+ }, {
25
+ capabilities: {
26
+ tools: {},
27
+ },
28
+ });
29
+ registerTools(server, { contextManager, tokenStore, hubiumClientFactory, safeMode, rateLimiter, fetchImpl: deps?.fetchImpl });
30
+ return server;
31
+ }
32
+ export async function runServerStdio(options) {
33
+ const server = createServer({ safeMode: options?.safeMode });
34
+ const transport = new StdioServerTransport();
35
+ await server.connect(transport);
36
+ }
37
+ export function registerTools(server, deps) {
38
+ const handlers = buildToolHandlers(deps);
39
+ server.setRequestHandler(ListToolsRequestSchema, handlers.listTools);
40
+ server.setRequestHandler(CallToolRequestSchema, handlers.callTool);
41
+ }
42
+ export function buildToolHandlers(deps) {
43
+ const redactor = new Redactor();
44
+ let cachedWhoami = null;
45
+ async function getWhoamiOnce(baseUrl, token) {
46
+ if (cachedWhoami) {
47
+ return cachedWhoami;
48
+ }
49
+ const client = deps.hubiumClientFactory({ baseUrl, token });
50
+ const whoami = await client.whoami();
51
+ cachedWhoami = whoami;
52
+ return whoami;
53
+ }
54
+ return {
55
+ listTools: async () => ({
56
+ tools: [
57
+ {
58
+ name: 'hubium.ping',
59
+ description: 'Health check for hubium-mcp server',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {},
63
+ additionalProperties: false,
64
+ },
65
+ },
66
+ {
67
+ name: 'hubium.get_context',
68
+ description: 'Get the current hubium-mcp context',
69
+ inputSchema: {
70
+ type: 'object',
71
+ properties: {},
72
+ additionalProperties: false,
73
+ },
74
+ },
75
+ {
76
+ name: 'hubium.set_context',
77
+ description: 'Update the hubium-mcp context',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ base_url: { type: 'string' },
82
+ workspace_id: { type: 'string' },
83
+ workspace_ids: { type: 'array', items: { type: 'string' } },
84
+ allow_insecure_http: { type: 'boolean' },
85
+ },
86
+ additionalProperties: false,
87
+ },
88
+ },
89
+ {
90
+ name: 'hubium.clear_context',
91
+ description: 'Clear the hubium-mcp context',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {},
95
+ additionalProperties: false,
96
+ },
97
+ },
98
+ {
99
+ name: 'hubium.whoami',
100
+ description: 'Return the authenticated Hubium actor identity',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {},
104
+ additionalProperties: false,
105
+ },
106
+ },
107
+ {
108
+ name: 'hubium.search',
109
+ description: 'Search Hubium cases with preview/full access',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ query: { type: 'string' },
114
+ mode: { type: 'string', enum: ['auto', 'preview', 'full'] },
115
+ workspace_id: { type: 'string' },
116
+ workspace_ids: { type: 'array', items: { type: 'string' } },
117
+ limit: { type: 'number' },
118
+ offset: { type: 'number' },
119
+ },
120
+ required: ['query'],
121
+ additionalProperties: false,
122
+ },
123
+ },
124
+ {
125
+ name: 'hubium.get_case',
126
+ description: 'Get a single Hubium case by id',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ case_id: { type: 'string' },
131
+ workspace_id: { type: 'string' },
132
+ workspace_ids: { type: 'array', items: { type: 'string' } },
133
+ },
134
+ required: ['case_id'],
135
+ additionalProperties: false,
136
+ },
137
+ },
138
+ {
139
+ name: 'hubium.create_case',
140
+ description: 'Create a new Hubium case',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ title: { type: 'string' },
145
+ description: { type: ['string', 'null'] },
146
+ final_solution: { type: ['string', 'null'] },
147
+ resolution_status: { type: 'string', enum: ['unsolved', 'workaround', 'solved'] },
148
+ visibility_intent: { type: 'string', enum: ['public', 'private'] },
149
+ workspace_id: { type: 'string' },
150
+ model_name: { type: ['string', 'null'] },
151
+ tags: { type: 'array', items: { type: 'string' } },
152
+ category: { type: ['string', 'null'] },
153
+ severity: { type: ['string', 'null'] },
154
+ environment: { type: ['string', 'null'] },
155
+ steps_to_reproduce: { type: ['string', 'null'] },
156
+ expected: { type: ['string', 'null'] },
157
+ actual: { type: ['string', 'null'] },
158
+ idempotency_key: { type: 'string' },
159
+ },
160
+ required: ['title', 'visibility_intent'],
161
+ additionalProperties: false,
162
+ },
163
+ },
164
+ {
165
+ name: 'hubium.doctor',
166
+ description: 'Diagnose hubium-mcp configuration and connectivity (no secrets printed)',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ base_url: { type: 'string' },
171
+ workspace_id: { type: 'string' },
172
+ },
173
+ additionalProperties: false,
174
+ },
175
+ },
176
+ {
177
+ name: 'hubium.feedback',
178
+ description: 'Append feedback to a Hubium case',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ case_id: { type: 'string' },
183
+ feedback_type: { type: 'string', enum: ['comment', 'analysis', 'resolution', 'signal'] },
184
+ content: { type: 'string' },
185
+ workspace_id: { type: 'string' },
186
+ visibility: { type: 'string', enum: ['internal', 'public'] },
187
+ idempotency_key: { type: 'string' },
188
+ },
189
+ required: ['case_id', 'feedback_type', 'content'],
190
+ additionalProperties: false,
191
+ },
192
+ },
193
+ ],
194
+ }),
195
+ callTool: async (request) => {
196
+ const rate = deps.rateLimiter.allow();
197
+ if (!rate.ok) {
198
+ return textResponse({ ok: false, error: 'rate_limited', retry_after_ms: rate.retryAfterMs });
199
+ }
200
+ const tool = request.params.name;
201
+ const local_request_id = crypto.randomUUID();
202
+ const start = Date.now();
203
+ const emitToolLog = (event, extra = {}) => {
204
+ const line = {
205
+ ts: new Date().toISOString(),
206
+ level: 'info',
207
+ event,
208
+ tool,
209
+ local_request_id,
210
+ duration_ms: Date.now() - start,
211
+ ...extra,
212
+ };
213
+ process.stderr.write(JSON.stringify(line) + '\n');
214
+ };
215
+ emitToolLog('tool_start');
216
+ let result;
217
+ try {
218
+ result = await (async () => {
219
+ switch (tool) {
220
+ case 'hubium.ping':
221
+ return textResponse({ ok: true });
222
+ case 'hubium.get_context': {
223
+ const context = await deps.contextManager.load();
224
+ const resolved = deps.contextManager.resolveBaseUrl();
225
+ return textResponse({ context, resolved_base_url: resolved });
226
+ }
227
+ case 'hubium.set_context': {
228
+ const input = normalizeArguments(request.params.arguments);
229
+ const allowInsecure = input.allow_insecure_http === true;
230
+ if (input.base_url !== undefined) {
231
+ const baseUrl = String(input.base_url);
232
+ if (baseUrl.trim().length === 0) {
233
+ throw new Error('base_url must be a non-empty string');
234
+ }
235
+ const url = new URL(baseUrl);
236
+ if (url.protocol !== 'https:' && !allowInsecure) {
237
+ throw new Error('base_url must be https:// unless allow_insecure_http is true');
238
+ }
239
+ }
240
+ const context = await deps.contextManager.save({
241
+ base_url: input.base_url ? String(input.base_url) : undefined,
242
+ workspace_id: input.workspace_id ? String(input.workspace_id) : undefined,
243
+ workspace_ids: Array.isArray(input.workspace_ids)
244
+ ? input.workspace_ids.map((value) => String(value))
245
+ : undefined,
246
+ });
247
+ deps.tokenStore.clearFollowupToken();
248
+ const resolved = deps.contextManager.resolveBaseUrl();
249
+ return textResponse({ context, resolved_base_url: resolved });
250
+ }
251
+ case 'hubium.clear_context': {
252
+ await deps.contextManager.clear();
253
+ deps.tokenStore.clearFollowupToken();
254
+ return textResponse({ ok: true });
255
+ }
256
+ case 'hubium.whoami': {
257
+ if (deps.safeMode) {
258
+ return textResponse({ ok: false, error: 'safe_mode' });
259
+ }
260
+ const context = await deps.contextManager.load();
261
+ const baseUrl = deps.contextManager.resolveBaseUrl();
262
+ if (!baseUrl) {
263
+ return textResponse({
264
+ ok: false,
265
+ error: 'missing_base_url',
266
+ next_step: {
267
+ action: 'set_base_url',
268
+ message_for_human: 'Set via hubium-mcp context set --base-url ... or HUBIUM_BASE_URL',
269
+ },
270
+ });
271
+ }
272
+ const tokenResult = await deps.tokenStore.getToken();
273
+ if (!tokenResult) {
274
+ return textResponse({
275
+ ok: false,
276
+ error: 'missing_token',
277
+ next_step: {
278
+ action: 'set_token',
279
+ message_for_human: 'Set HUBIUM_TOKEN or store via keychain in later ticket.',
280
+ },
281
+ });
282
+ }
283
+ const client = deps.hubiumClientFactory({ baseUrl, token: tokenResult.token });
284
+ const whoami = await client.whoami();
285
+ return textResponse({
286
+ ok: true,
287
+ actor: whoami.actor,
288
+ base_url: baseUrl,
289
+ workspace_defaults: {
290
+ workspace_id: context.workspace_id,
291
+ workspace_ids: context.workspace_ids,
292
+ },
293
+ });
294
+ }
295
+ case 'hubium.search': {
296
+ const input = normalizeArguments(request.params.arguments);
297
+ const queryValue = typeof input.query === 'string' ? input.query.trim() : '';
298
+ if (!queryValue) {
299
+ throw new Error('query is required');
300
+ }
301
+ enforceMaxLength('query', queryValue, 2000);
302
+ if (input.workspace_id !== undefined && input.workspace_ids !== undefined) {
303
+ throw new Error('Provide only one of workspace_id or workspace_ids');
304
+ }
305
+ const modeRaw = typeof input.mode === 'string' ? input.mode : 'auto';
306
+ const mode = modeRaw === 'preview' || modeRaw === 'full' || modeRaw === 'auto' ? modeRaw : 'auto';
307
+ const effectiveMode = deps.safeMode ? 'preview' : mode;
308
+ const limit = sanitizeLimit(input.limit);
309
+ const offset = sanitizeOffset(input.offset);
310
+ const context = await deps.contextManager.load();
311
+ const baseUrl = deps.contextManager.resolveBaseUrl();
312
+ if (!baseUrl) {
313
+ return textResponse({
314
+ ok: false,
315
+ error: 'missing_base_url',
316
+ next_step: {
317
+ action: 'set_base_url',
318
+ message_for_human: 'Set via hubium-mcp context set --base-url ... or HUBIUM_BASE_URL',
319
+ },
320
+ });
321
+ }
322
+ const tokenResult = deps.safeMode ? null : await deps.tokenStore.getToken();
323
+ const hasAuth = Boolean(tokenResult) && !deps.safeMode;
324
+ const followupToken = effectiveMode === 'preview' ? null : deps.tokenStore.getFollowupToken();
325
+ const workspaceSelection = resolveWorkspaceSelection({
326
+ input,
327
+ context,
328
+ hasAuth,
329
+ baseUrl,
330
+ token: hasAuth ? tokenResult?.token ?? null : null,
331
+ getWhoamiOnce,
332
+ });
333
+ const resolvedWorkspaces = await workspaceSelection;
334
+ const params = {
335
+ query: queryValue,
336
+ limit,
337
+ offset,
338
+ ...resolvedWorkspaces,
339
+ };
340
+ const runSearch = async (token) => {
341
+ const client = deps.hubiumClientFactory({ baseUrl, token: token ?? undefined });
342
+ return client.search(params, followupToken ? { followupToken } : undefined);
343
+ };
344
+ if (effectiveMode === 'preview') {
345
+ const response = await runSearch(null);
346
+ persistFollowupToken(response, deps.tokenStore);
347
+ logFollowupUsed(response, redactor, await collectSecrets(deps.tokenStore));
348
+ return textResponse(response);
349
+ }
350
+ if (effectiveMode === 'full') {
351
+ if (!tokenResult) {
352
+ return textResponse({ ok: false, error: 'missing_token' });
353
+ }
354
+ const response = await runSearch(tokenResult.token);
355
+ persistFollowupToken(response, deps.tokenStore);
356
+ logFollowupUsed(response, redactor, await collectSecrets(deps.tokenStore));
357
+ return textResponse(response);
358
+ }
359
+ if (tokenResult) {
360
+ try {
361
+ const response = await runSearch(tokenResult.token);
362
+ persistFollowupToken(response, deps.tokenStore);
363
+ logFollowupUsed(response, redactor, await collectSecrets(deps.tokenStore));
364
+ return textResponse(response);
365
+ }
366
+ catch (error) {
367
+ if (error instanceof HubiumClientError && (error.status === 401 || error.status === 403)) {
368
+ const response = await runSearch(null);
369
+ persistFollowupToken(response, deps.tokenStore);
370
+ logFollowupUsed(response, redactor, await collectSecrets(deps.tokenStore));
371
+ return textResponse(response);
372
+ }
373
+ throw error;
374
+ }
375
+ }
376
+ const response = await runSearch(null);
377
+ persistFollowupToken(response, deps.tokenStore);
378
+ logFollowupUsed(response, redactor, await collectSecrets(deps.tokenStore));
379
+ return textResponse(response);
380
+ }
381
+ case 'hubium.get_case': {
382
+ const input = normalizeArguments(request.params.arguments);
383
+ const caseId = typeof input.case_id === 'string' ? input.case_id.trim() : '';
384
+ if (!caseId) {
385
+ throw new Error('case_id is required');
386
+ }
387
+ if (input.workspace_id !== undefined && input.workspace_ids !== undefined) {
388
+ throw new Error('Provide only one of workspace_id or workspace_ids');
389
+ }
390
+ const context = await deps.contextManager.load();
391
+ const baseUrl = deps.contextManager.resolveBaseUrl();
392
+ if (!baseUrl) {
393
+ return textResponse({
394
+ ok: false,
395
+ error: 'missing_base_url',
396
+ next_step: {
397
+ action: 'set_base_url',
398
+ message_for_human: 'Set via hubium-mcp context set --base-url ... or HUBIUM_BASE_URL',
399
+ },
400
+ });
401
+ }
402
+ const tokenResult = deps.safeMode ? null : await deps.tokenStore.getToken();
403
+ const resolvedWorkspaces = await resolveWorkspaceSelection({
404
+ input,
405
+ context,
406
+ hasAuth: Boolean(tokenResult) && !deps.safeMode,
407
+ baseUrl,
408
+ token: deps.safeMode ? null : tokenResult?.token ?? null,
409
+ getWhoamiOnce,
410
+ });
411
+ const client = deps.hubiumClientFactory({ baseUrl, token: deps.safeMode ? undefined : tokenResult?.token });
412
+ const response = await client.getCase(caseId, resolvedWorkspaces);
413
+ return textResponse(response);
414
+ }
415
+ case 'hubium.create_case': {
416
+ if (deps.safeMode) {
417
+ return textResponse({ ok: false, error: 'safe_mode' });
418
+ }
419
+ const input = normalizeArguments(request.params.arguments);
420
+ const title = typeof input.title === 'string' ? input.title.trim() : '';
421
+ if (!title) {
422
+ throw new Error('title is required');
423
+ }
424
+ enforceMaxLength('title', title, 200);
425
+ const visibility = typeof input.visibility_intent === 'string' ? input.visibility_intent : '';
426
+ if (visibility !== 'public' && visibility !== 'private') {
427
+ throw new Error('visibility_intent is required');
428
+ }
429
+ if (input.workspace_ids !== undefined) {
430
+ throw new Error('workspace_ids is not supported for create_case');
431
+ }
432
+ const context = await deps.contextManager.load();
433
+ const baseUrl = deps.contextManager.resolveBaseUrl();
434
+ if (!baseUrl) {
435
+ return textResponse({
436
+ ok: false,
437
+ error: 'missing_base_url',
438
+ next_step: {
439
+ action: 'set_base_url',
440
+ message_for_human: 'Set via hubium-mcp context set --base-url ... or HUBIUM_BASE_URL',
441
+ },
442
+ });
443
+ }
444
+ const tokenResult = deps.safeMode ? null : await deps.tokenStore.getToken();
445
+ if (!tokenResult) {
446
+ return textResponse({ ok: false, error: 'missing_token' });
447
+ }
448
+ if (!input.workspace_id && context.workspace_ids && context.workspace_ids.length > 0) {
449
+ return textResponse({ ok: false, error: 'missing_workspace' });
450
+ }
451
+ const resolved = await resolveWorkspaceSelection({
452
+ input: { workspace_id: input.workspace_id },
453
+ context,
454
+ hasAuth: true,
455
+ baseUrl,
456
+ token: tokenResult.token,
457
+ getWhoamiOnce,
458
+ });
459
+ if (!resolved.workspace_id) {
460
+ return textResponse({ ok: false, error: 'missing_workspace' });
461
+ }
462
+ const tags = Array.isArray(input.tags)
463
+ ? input.tags.map((value) => String(value))
464
+ : undefined;
465
+ enforceTags(tags);
466
+ enforceOptionalMaxLength('description', input.description, 10000);
467
+ enforceOptionalMaxLength('final_solution', input.final_solution, 10000);
468
+ enforceOptionalMaxLength('environment', input.environment, 10000);
469
+ enforceOptionalMaxLength('steps_to_reproduce', input.steps_to_reproduce, 10000);
470
+ enforceOptionalMaxLength('expected', input.expected, 10000);
471
+ enforceOptionalMaxLength('actual', input.actual, 10000);
472
+ const payload = {
473
+ title,
474
+ description: normalizeOptionalString(input.description),
475
+ final_solution: normalizeOptionalString(input.final_solution),
476
+ resolution_status: normalizeOptionalString(input.resolution_status),
477
+ visibility_intent: visibility,
478
+ workspace_id: resolved.workspace_id,
479
+ model_name: normalizeOptionalString(input.model_name),
480
+ tags,
481
+ category: normalizeOptionalString(input.category),
482
+ severity: normalizeOptionalString(input.severity),
483
+ environment: normalizeOptionalString(input.environment),
484
+ steps_to_reproduce: normalizeOptionalString(input.steps_to_reproduce),
485
+ expected: normalizeOptionalString(input.expected),
486
+ actual: normalizeOptionalString(input.actual),
487
+ source_agent: 'hubium-mcp',
488
+ };
489
+ const idempotencyKey = typeof input.idempotency_key === 'string' && input.idempotency_key.trim().length > 0
490
+ ? input.idempotency_key.trim()
491
+ : undefined;
492
+ const client = deps.hubiumClientFactory({ baseUrl, token: tokenResult.token });
493
+ try {
494
+ const response = await client.createCase(payload, idempotencyKey ? { idempotencyKey } : undefined);
495
+ return textResponse(response);
496
+ }
497
+ catch (error) {
498
+ const secrets = await collectSecrets(deps.tokenStore, idempotencyKey ? [idempotencyKey] : []);
499
+ return safeErrorResponse(error, redactor, secrets);
500
+ }
501
+ }
502
+ case 'hubium.doctor': {
503
+ const input = normalizeArguments(request.params.arguments);
504
+ const baseUrl = typeof input.base_url === 'string' ? input.base_url.trim() || undefined : undefined;
505
+ const workspaceId = typeof input.workspace_id === 'string' ? input.workspace_id.trim() || undefined : undefined;
506
+ const result = await runDoctor({ base_url: baseUrl, workspace_id: workspaceId }, {
507
+ contextManager: deps.contextManager,
508
+ tokenStore: deps.tokenStore,
509
+ getSafeMode: () => deps.safeMode,
510
+ fetchImpl: deps.fetchImpl,
511
+ });
512
+ return textResponse(result);
513
+ }
514
+ case 'hubium.feedback': {
515
+ if (deps.safeMode) {
516
+ return textResponse({ ok: false, error: 'safe_mode' });
517
+ }
518
+ const input = normalizeArguments(request.params.arguments);
519
+ const caseId = typeof input.case_id === 'string' ? input.case_id.trim() : '';
520
+ if (!caseId) {
521
+ throw new Error('case_id is required');
522
+ }
523
+ const feedbackType = typeof input.feedback_type === 'string' ? input.feedback_type : '';
524
+ if (!['comment', 'analysis', 'resolution', 'signal'].includes(feedbackType)) {
525
+ throw new Error('feedback_type is required');
526
+ }
527
+ const content = typeof input.content === 'string' ? input.content.trim() : '';
528
+ if (!content) {
529
+ throw new Error('content is required');
530
+ }
531
+ enforceMaxLength('content', content, 10000);
532
+ if (input.workspace_ids !== undefined) {
533
+ throw new Error('workspace_ids is not supported for feedback');
534
+ }
535
+ const context = await deps.contextManager.load();
536
+ const baseUrl = deps.contextManager.resolveBaseUrl();
537
+ if (!baseUrl) {
538
+ return textResponse({
539
+ ok: false,
540
+ error: 'missing_base_url',
541
+ next_step: {
542
+ action: 'set_base_url',
543
+ message_for_human: 'Set via hubium-mcp context set --base-url ... or HUBIUM_BASE_URL',
544
+ },
545
+ });
546
+ }
547
+ const tokenResult = await deps.tokenStore.getToken();
548
+ if (!tokenResult) {
549
+ return textResponse({ ok: false, error: 'missing_token' });
550
+ }
551
+ if (!input.workspace_id && context.workspace_ids && context.workspace_ids.length > 0) {
552
+ return textResponse({ ok: false, error: 'missing_workspace' });
553
+ }
554
+ const resolved = await resolveWorkspaceSelection({
555
+ input: { workspace_id: input.workspace_id },
556
+ context,
557
+ hasAuth: true,
558
+ baseUrl,
559
+ token: tokenResult.token,
560
+ getWhoamiOnce,
561
+ });
562
+ if (!resolved.workspace_id) {
563
+ return textResponse({ ok: false, error: 'missing_workspace' });
564
+ }
565
+ const visibilityRaw = typeof input.visibility === 'string' ? input.visibility : undefined;
566
+ if (visibilityRaw !== undefined && visibilityRaw !== 'internal' && visibilityRaw !== 'public') {
567
+ throw new Error('visibility must be one of: internal, public');
568
+ }
569
+ const payload = {
570
+ feedback_type: feedbackType,
571
+ content,
572
+ workspace_id: resolved.workspace_id,
573
+ visibility: visibilityRaw,
574
+ };
575
+ const idempotencyKey = typeof input.idempotency_key === 'string' && input.idempotency_key.trim().length > 0
576
+ ? input.idempotency_key.trim()
577
+ : undefined;
578
+ const client = deps.hubiumClientFactory({ baseUrl, token: tokenResult.token });
579
+ try {
580
+ const response = await client.addFeedback(caseId, payload, idempotencyKey ? { idempotencyKey } : undefined);
581
+ return textResponse(response);
582
+ }
583
+ catch (error) {
584
+ const secrets = await collectSecrets(deps.tokenStore, idempotencyKey ? [idempotencyKey] : []);
585
+ return safeErrorResponse(error, redactor, secrets);
586
+ }
587
+ }
588
+ default:
589
+ return textResponse({ error: 'Unknown tool' });
590
+ }
591
+ })();
592
+ try {
593
+ const parsed = JSON.parse(result.content[0].text);
594
+ if (parsed.error === 'hubium_client_error') {
595
+ const backendRequestId = parsed.requestId ?? parsed.request_id;
596
+ emitToolLog('tool_error', backendRequestId ? { backend_request_id: backendRequestId } : {});
597
+ }
598
+ else {
599
+ emitToolLog('tool_end');
600
+ }
601
+ }
602
+ catch {
603
+ emitToolLog('tool_end');
604
+ }
605
+ return result;
606
+ }
607
+ catch (error) {
608
+ emitToolLog('tool_error', {
609
+ backend_request_id: error instanceof HubiumClientError ? error.requestId : undefined,
610
+ });
611
+ const secrets = await collectSecrets(deps.tokenStore);
612
+ return safeErrorResponse(error, redactor, secrets);
613
+ }
614
+ },
615
+ };
616
+ }
617
+ function normalizeArguments(input) {
618
+ if (!input || typeof input !== 'object') {
619
+ return {};
620
+ }
621
+ return input;
622
+ }
623
+ function textResponse(payload) {
624
+ return {
625
+ content: [
626
+ {
627
+ type: 'text',
628
+ text: JSON.stringify(payload),
629
+ },
630
+ ],
631
+ };
632
+ }
633
+ function sanitizeLimit(limit) {
634
+ if (limit === undefined) {
635
+ return undefined;
636
+ }
637
+ if (typeof limit !== 'number' || Number.isNaN(limit)) {
638
+ throw new Error('limit must be a number');
639
+ }
640
+ return Math.max(0, Math.min(100, Math.floor(limit)));
641
+ }
642
+ function sanitizeOffset(offset) {
643
+ if (offset === undefined) {
644
+ return undefined;
645
+ }
646
+ if (typeof offset !== 'number' || Number.isNaN(offset)) {
647
+ throw new Error('offset must be a number');
648
+ }
649
+ return Math.max(0, Math.min(10_000, Math.floor(offset)));
650
+ }
651
+ async function resolveWorkspaceSelection(params) {
652
+ if (typeof params.input.workspace_id === 'string' && params.input.workspace_id.trim().length > 0) {
653
+ return { workspace_id: params.input.workspace_id.trim() };
654
+ }
655
+ if (Array.isArray(params.input.workspace_ids)) {
656
+ const ids = params.input.workspace_ids
657
+ .filter((value) => typeof value === 'string')
658
+ .map((value) => value.trim())
659
+ .filter((value) => value.length > 0);
660
+ if (ids.length > 0) {
661
+ return { workspace_ids: ids };
662
+ }
663
+ }
664
+ if (params.context.workspace_ids && params.context.workspace_ids.length > 0) {
665
+ return { workspace_ids: params.context.workspace_ids };
666
+ }
667
+ if (params.context.workspace_id && params.context.workspace_id.trim().length > 0) {
668
+ return { workspace_id: params.context.workspace_id };
669
+ }
670
+ if (params.hasAuth && params.token) {
671
+ const whoami = await params.getWhoamiOnce(params.baseUrl, params.token);
672
+ const workspaces = Array.isArray(whoami.workspaces) ? whoami.workspaces : [];
673
+ if (workspaces.length > 0) {
674
+ const first = workspaces[0];
675
+ if (first?.id) {
676
+ return { workspace_id: String(first.id) };
677
+ }
678
+ }
679
+ }
680
+ return {};
681
+ }
682
+ function persistFollowupToken(response, tokenStore) {
683
+ const followupToken = response.search_session?.followup_token;
684
+ if (followupToken && followupToken.trim().length > 0) {
685
+ tokenStore.setFollowupToken(followupToken);
686
+ }
687
+ }
688
+ function logFollowupUsed(response, redactor, secrets) {
689
+ if (!process.env.DEBUG) {
690
+ return;
691
+ }
692
+ if (response.search_session?.is_followup) {
693
+ const safe = redactor.redact({ search_session_id: response.search_session.id, is_followup: true }, secrets);
694
+ process.stderr.write(`[hubium-mcp] followup session ${JSON.stringify(safe)}\n`);
695
+ }
696
+ }
697
+ async function collectSecrets(tokenStore, extra = []) {
698
+ const secrets = [];
699
+ try {
700
+ const tokenResult = await tokenStore.getToken();
701
+ if (tokenResult?.token) {
702
+ secrets.push(tokenResult.token);
703
+ }
704
+ }
705
+ catch {
706
+ // ignore token resolution errors
707
+ }
708
+ const followup = tokenStore.getFollowupToken();
709
+ if (followup) {
710
+ secrets.push(followup);
711
+ }
712
+ const envTokens = [
713
+ process.env.HUBIUM_TOKEN,
714
+ process.env.HUBIUM_API_TOKEN,
715
+ ].filter((value) => typeof value === 'string' && value.trim().length > 0);
716
+ secrets.push(...envTokens, ...extra);
717
+ return secrets;
718
+ }
719
+ function redactString(redactor, value, secrets) {
720
+ if (secrets.length === 0) {
721
+ return value;
722
+ }
723
+ const result = redactor.redact(value, secrets);
724
+ return typeof result === 'string' ? result : value;
725
+ }
726
+ function safeErrorResponse(error, redactor, secrets) {
727
+ if (error instanceof HubiumClientError) {
728
+ const message = redactString(redactor, error.message, secrets);
729
+ return textResponse({
730
+ error: 'hubium_client_error',
731
+ kind: error.kind,
732
+ status: error.status,
733
+ requestId: error.requestId,
734
+ message,
735
+ });
736
+ }
737
+ const message = error instanceof Error ? error.message : 'Unknown error';
738
+ return textResponse({ error: redactString(redactor, message, secrets) });
739
+ }
740
+ function normalizeOptionalString(value) {
741
+ if (value === null) {
742
+ return null;
743
+ }
744
+ if (value === undefined) {
745
+ return undefined;
746
+ }
747
+ if (typeof value !== 'string') {
748
+ return undefined;
749
+ }
750
+ const trimmed = value.trim();
751
+ return trimmed.length > 0 ? trimmed : null;
752
+ }
753
+ function enforceMaxLength(field, value, max) {
754
+ if (value.length > max) {
755
+ throw new Error(`${field} exceeds max length ${max}`);
756
+ }
757
+ }
758
+ function enforceOptionalMaxLength(field, value, max) {
759
+ if (typeof value !== 'string') {
760
+ return;
761
+ }
762
+ const trimmed = value.trim();
763
+ if (trimmed.length > max) {
764
+ throw new Error(`${field} exceeds max length ${max}`);
765
+ }
766
+ }
767
+ function enforceTags(tags) {
768
+ if (!tags) {
769
+ return;
770
+ }
771
+ if (tags.length > 50) {
772
+ throw new Error('tags exceeds max length 50');
773
+ }
774
+ for (const tag of tags) {
775
+ if (tag.length > 64) {
776
+ throw new Error('tag exceeds max length 64');
777
+ }
778
+ }
779
+ }
780
+ export function createRateLimiterFromEnv() {
781
+ const perMin = Number.parseInt(process.env.HUBIUM_MCP_RATE_LIMIT_PER_MIN ?? '60', 10);
782
+ const limit = Number.isNaN(perMin) || perMin <= 0 ? 60 : perMin;
783
+ return new LocalRateLimiter(limit, 60_000);
784
+ }
785
+ export class LocalRateLimiter {
786
+ limit;
787
+ windowMs;
788
+ timestamps = [];
789
+ constructor(limit, windowMs) {
790
+ this.limit = limit;
791
+ this.windowMs = windowMs;
792
+ }
793
+ allow() {
794
+ const now = Date.now();
795
+ while (this.timestamps.length > 0 && now - this.timestamps[0] >= this.windowMs) {
796
+ this.timestamps.shift();
797
+ }
798
+ if (this.timestamps.length >= this.limit) {
799
+ const retryAfterMs = Math.max(0, this.windowMs - (now - this.timestamps[0]));
800
+ return { ok: false, retryAfterMs };
801
+ }
802
+ this.timestamps.push(now);
803
+ return { ok: true };
804
+ }
805
+ }
806
+ function getSafeMode() {
807
+ return process.env.HUBIUM_SAFE_MODE === '1';
808
+ }
809
+ //# sourceMappingURL=server.js.map