sequentum-mcp 1.1.4 → 1.2.2

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 (60) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +104 -3
  3. package/dist/{api-client.d.ts → api/api-client.d.ts} +106 -4
  4. package/dist/api/api-client.d.ts.map +1 -0
  5. package/dist/{api-client.js → api/api-client.js} +345 -99
  6. package/dist/api/api-client.js.map +1 -0
  7. package/dist/{types.d.ts → api/types.d.ts} +122 -12
  8. package/dist/api/types.d.ts.map +1 -0
  9. package/dist/{types.js → api/types.js} +57 -0
  10. package/dist/api/types.js.map +1 -0
  11. package/dist/index.js +11 -1737
  12. package/dist/index.js.map +1 -1
  13. package/dist/server/cors.d.ts +33 -0
  14. package/dist/server/cors.d.ts.map +1 -0
  15. package/dist/server/cors.js +72 -0
  16. package/dist/server/cors.js.map +1 -0
  17. package/dist/server/handlers.d.ts +49 -0
  18. package/dist/server/handlers.d.ts.map +1 -0
  19. package/dist/server/handlers.js +1031 -0
  20. package/dist/server/handlers.js.map +1 -0
  21. package/dist/server/http-server.d.ts +13 -0
  22. package/dist/server/http-server.d.ts.map +1 -0
  23. package/dist/server/http-server.js +393 -0
  24. package/dist/server/http-server.js.map +1 -0
  25. package/dist/server/policies.d.ts +19 -0
  26. package/dist/server/policies.d.ts.map +1 -0
  27. package/dist/server/policies.js +32 -0
  28. package/dist/server/policies.js.map +1 -0
  29. package/dist/server/prompts.d.ts +19 -0
  30. package/dist/server/prompts.d.ts.map +1 -0
  31. package/dist/server/prompts.js +464 -0
  32. package/dist/server/prompts.js.map +1 -0
  33. package/dist/server/resources.d.ts +26 -0
  34. package/dist/server/resources.d.ts.map +1 -0
  35. package/dist/server/resources.js +348 -0
  36. package/dist/server/resources.js.map +1 -0
  37. package/dist/server/tools.d.ts +9 -0
  38. package/dist/server/tools.d.ts.map +1 -0
  39. package/dist/server/tools.js +977 -0
  40. package/dist/server/tools.js.map +1 -0
  41. package/dist/utils/oauth-metadata.d.ts.map +1 -0
  42. package/dist/utils/oauth-metadata.js.map +1 -0
  43. package/dist/{validation.d.ts → utils/validation.d.ts} +25 -2
  44. package/dist/utils/validation.d.ts.map +1 -0
  45. package/dist/{validation.js → utils/validation.js} +43 -3
  46. package/dist/utils/validation.js.map +1 -0
  47. package/docs/prompts-reference.md +370 -0
  48. package/docs/resources-reference.md +300 -0
  49. package/docs/tool-reference.md +244 -2
  50. package/package.json +4 -3
  51. package/dist/api-client.d.ts.map +0 -1
  52. package/dist/api-client.js.map +0 -1
  53. package/dist/oauth-metadata.d.ts.map +0 -1
  54. package/dist/oauth-metadata.js.map +0 -1
  55. package/dist/types.d.ts.map +0 -1
  56. package/dist/types.js.map +0 -1
  57. package/dist/validation.d.ts.map +0 -1
  58. package/dist/validation.js.map +0 -1
  59. /package/dist/{oauth-metadata.d.ts → utils/oauth-metadata.d.ts} +0 -0
  60. /package/dist/{oauth-metadata.js → utils/oauth-metadata.js} +0 -0
package/dist/index.js CHANGED
@@ -27,15 +27,10 @@
27
27
  * via the Authorization header on each request.
28
28
  */
29
29
  import { createRequire } from "module";
30
- import { randomUUID } from "crypto";
31
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
32
30
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
34
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
35
- import express from "express";
36
- import { SequentumApiClient } from "./api-client.js";
37
- import { getDefaultDateRange, validateBoolean, validateDateRange, validateEnum, validateISODate, validateNumber, validateStartTimeInFuture, validateString, } from "./validation.js";
38
- import { buildOAuthMetadata, SUPPORTED_SCOPES } from "./oauth-metadata.js";
31
+ import { SequentumApiClient } from "./api/api-client.js";
32
+ import { createMcpServer } from "./server/handlers.js";
33
+ import { startHttpServer } from "./server/http-server.js";
39
34
  // Import version from package.json
40
35
  const require = createRequire(import.meta.url);
41
36
  const { version } = require("../package.json");
@@ -43,7 +38,7 @@ const { version } = require("../package.json");
43
38
  const DEFAULT_API_URL = "https://dashboard.sequentum.com";
44
39
  const API_BASE_URL = process.env.SEQUENTUM_API_URL || DEFAULT_API_URL;
45
40
  const API_KEY = process.env.SEQUENTUM_API_KEY;
46
- const DEBUG = process.env.DEBUG === '1';
41
+ const DEBUG = process.env.DEBUG === "1";
47
42
  // Transport configuration
48
43
  // - "stdio": For Claude Code and local development (default)
49
44
  // - "http": For Claude Connectors (claude.ai, Claude Desktop)
@@ -55,15 +50,12 @@ const HTTP_HOST = process.env.HOST || "0.0.0.0";
55
50
  // - HTTP mode: Uses OAuth2 via Claude's infrastructure (for Claude Connectors)
56
51
  let authMode;
57
52
  if (TRANSPORT_MODE === "http") {
58
- // HTTP mode: OAuth2 is required and handled by Claude's infrastructure
59
- // Tokens will be passed in Authorization header of each request
60
53
  authMode = "oauth2";
61
54
  if (DEBUG) {
62
- console.error(`[DEBUG] HTTP mode: OAuth2 tokens will be received via request headers`);
55
+ console.error("[DEBUG] HTTP mode: OAuth2 tokens will be received via request headers");
63
56
  }
64
57
  }
65
58
  else {
66
- // stdio mode: API Key is required
67
59
  if (!API_KEY) {
68
60
  console.error("Error: API Key required for stdio mode");
69
61
  console.error('Set SEQUENTUM_API_KEY="sk-your-api-key-here"');
@@ -73,1458 +65,19 @@ else {
73
65
  }
74
66
  authMode = "apikey";
75
67
  if (DEBUG) {
76
- console.error(`[DEBUG] Using API Key authentication`);
68
+ console.error("[DEBUG] Using API Key authentication");
77
69
  }
78
70
  }
79
71
  // Debug: Log environment configuration (only when DEBUG=1)
80
72
  if (DEBUG) {
81
73
  console.error(`[DEBUG] TRANSPORT_MODE = ${TRANSPORT_MODE}`);
82
- console.error(`[DEBUG] API_BASE_URL = ${API_BASE_URL}${!process.env.SEQUENTUM_API_URL ? ' (default)' : ''}`);
74
+ console.error(`[DEBUG] API_BASE_URL = ${API_BASE_URL}${!process.env.SEQUENTUM_API_URL ? " (default)" : ""}`);
83
75
  console.error(`[DEBUG] Auth Mode = ${authMode}`);
84
76
  if (TRANSPORT_MODE === "http") {
85
77
  console.error(`[DEBUG] HTTP_PORT = ${HTTP_PORT}`);
86
78
  console.error(`[DEBUG] HTTP_HOST = ${HTTP_HOST}`);
87
79
  }
88
80
  }
89
- // Create API client
90
- // - stdio mode: initialized with API key
91
- // - HTTP mode: initialized without auth (tokens come via request headers)
92
- const client = new SequentumApiClient(API_BASE_URL, authMode === "apikey" ? API_KEY : null);
93
- // ==========================================
94
- // Input Validation Helpers
95
- // ==========================================
96
- // ==========================================
97
- // Response Helpers
98
- // ==========================================
99
- /**
100
- * Map RunStatus numeric value to human-readable string
101
- */
102
- function getRunStatusLabel(status) {
103
- const statusMap = {
104
- 0: "Invalid",
105
- 1: "Running",
106
- 2: "Exporting",
107
- 3: "Starting",
108
- 4: "Queuing",
109
- 5: "Stopping",
110
- 6: "Failure",
111
- 7: "Failed",
112
- 8: "Stopped",
113
- 9: "Completed",
114
- 10: "Success",
115
- 11: "Skipped",
116
- 12: "Waiting",
117
- };
118
- if (status === undefined || status === null) {
119
- return "Never Run";
120
- }
121
- return statusMap[status] ?? `Unknown (${status})`;
122
- }
123
- /**
124
- * Transform agent list to summary format for display
125
- */
126
- function summarizeAgents(agents) {
127
- return agents.map((a) => ({
128
- id: a.id,
129
- name: a.name,
130
- description: a.description,
131
- status: getRunStatusLabel(a.status),
132
- configType: a.configType,
133
- version: a.version,
134
- lastActivity: a.lastActivity,
135
- }));
136
- }
137
- // ==========================================
138
- // Tool Definitions
139
- // ==========================================
140
- const tools = [
141
- // Agent Tools
142
- {
143
- name: "list_agents",
144
- description: "List web scraping agents with IDs, names, status, and configuration. " +
145
- "USE THIS FIRST to discover available agents before running or managing them. " +
146
- "Answers: 'What agents do I have?', 'Show me my scrapers', 'List all completed agents'. " +
147
- "Returns: Array of agent summaries with id, name, status (last run status), configType, version, lastActivity. " +
148
- "Pagination always applied (defaults: pageIndex=1, recordsPerPage=50). " +
149
- "TIP: Use 'search' param to find agents by name, or 'status' to filter by last run status (Completed, Failed, etc.).",
150
- inputSchema: {
151
- type: "object",
152
- properties: {
153
- status: {
154
- type: "number",
155
- enum: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
156
- description: "Filter by last run status: 0=Invalid, 1=Running, 2=Exporting, 3=Starting, 4=Queuing, 5=Stopping, 6=Failure, 7=Failed, 8=Stopped, 9=Completed, 10=Success, 11=Skipped, 12=Waiting. Agents that never ran have null status.",
157
- },
158
- spaceId: {
159
- type: "number",
160
- description: "Filter by space ID. Use list_spaces first to find space IDs.",
161
- },
162
- search: {
163
- type: "string",
164
- description: "Search by agent name (case-insensitive partial match). Example: 'amazon' finds 'Amazon Product Scraper'.",
165
- },
166
- configType: {
167
- type: "string",
168
- enum: ["Agent", "Command", "Api", "Shared"],
169
- description: "Filter by type. 'Agent' = web scrapers, 'Command' = data inputs, 'Api' = API configs, 'Shared' = reusable components.",
170
- },
171
- sortColumn: {
172
- type: "string",
173
- enum: ["name", "lastActivity", "created", "updated", "status", "configType"],
174
- description: "Column to sort by. 'lastActivity' shows recently run agents first (with sortOrder=1).",
175
- },
176
- sortOrder: {
177
- type: "string",
178
- enum: ["asc", "desc"],
179
- description: "Sort direction. 'desc' = newest/Z first (descending), 'asc' = oldest/A first (ascending, default).",
180
- },
181
- pageIndex: {
182
- type: "number",
183
- description: "Page number (1-based). Defaults to 1. Use with recordsPerPage to paginate large result sets.",
184
- },
185
- recordsPerPage: {
186
- type: "number",
187
- description: "Results per page. Defaults to 50. Max recommended: 100.",
188
- },
189
- },
190
- required: [],
191
- },
192
- annotations: {
193
- readOnlyHint: true,
194
- },
195
- },
196
- {
197
- name: "get_agent",
198
- description: "Get detailed information about a specific agent including its configuration, input parameters, and documentation. " +
199
- "USE AFTER list_agents or search_agents when you need full details for a specific agent. " +
200
- "Answers: 'Tell me about agent X', 'What parameters does this agent need?', 'Show agent configuration'. " +
201
- "Returns: Full agent details including inputParameters (what inputs the agent accepts), description, documentation, startUrl. " +
202
- "REQUIRED: You must have the agentId first (get it from list_agents or search_agents).",
203
- inputSchema: {
204
- type: "object",
205
- properties: {
206
- agentId: { type: "number", description: "The unique ID of the agent. Get this from list_agents or search_agents." },
207
- },
208
- required: ["agentId"],
209
- },
210
- annotations: {
211
- readOnlyHint: true,
212
- },
213
- },
214
- {
215
- name: "search_agents",
216
- description: "Search for agents by name or description (case-insensitive partial match). " +
217
- "FASTER than list_agents when user mentions a specific agent name. " +
218
- "Answers: 'Find the Amazon scraper', 'Which agent handles product data?', 'Search for pricing agents'. " +
219
- "Returns: Matching agents with id, name, status, configType. " +
220
- "TIP: Prefer this over list_agents when user mentions an agent by name.",
221
- inputSchema: {
222
- type: "object",
223
- properties: {
224
- query: { type: "string", description: "Search term to match against agent names and descriptions. Case-insensitive." },
225
- maxRecords: { type: "number", description: "Maximum results to return. Default: 50, Max: 1000." },
226
- },
227
- required: ["query"],
228
- },
229
- annotations: {
230
- readOnlyHint: true,
231
- },
232
- },
233
- // Run Tools
234
- {
235
- name: "get_agent_runs",
236
- description: "Get execution history for an agent showing past runs with status, timing, and records extracted. " +
237
- "Answers: 'When did agent X last run?', 'Show run history', 'How many records were extracted?', 'Did the agent fail?'. " +
238
- "Returns: Array of runs with id, status (Running/Completed/Failed/etc), startTime, endTime, recordsExtracted, recordsExported, errorMessage. " +
239
- "TIP: Check the most recent run's status to see if agent is currently running or recently completed. " +
240
- "NEXT STEP: Use get_run_files to see output files from a completed run.",
241
- inputSchema: {
242
- type: "object",
243
- properties: {
244
- agentId: { type: "number", description: "The unique ID of the agent. Get this from list_agents or search_agents." },
245
- maxRecords: { type: "number", description: "Maximum number of runs to return. Default: 50. Use smaller values for faster response." },
246
- },
247
- required: ["agentId"],
248
- },
249
- annotations: {
250
- readOnlyHint: true,
251
- },
252
- },
253
- {
254
- name: "get_run_status",
255
- description: "Get the current status of a specific run. FASTER than get_agent_runs when you only need one run's status. " +
256
- "Answers: 'Is run 123 still running?', 'Did that run complete?', 'Check run status'. " +
257
- "Returns: Single run with status, timing, records extracted. " +
258
- "USE AFTER start_agent to monitor a run you just started. " +
259
- "Status values: Running, Completed, Failed, CompletedWithErrors, Stopped, Queued.",
260
- inputSchema: {
261
- type: "object",
262
- properties: {
263
- agentId: { type: "number", description: "The unique ID of the agent." },
264
- runId: { type: "number", description: "The run ID returned by start_agent or found in get_agent_runs." },
265
- },
266
- required: ["agentId", "runId"],
267
- },
268
- annotations: {
269
- readOnlyHint: true,
270
- },
271
- },
272
- {
273
- name: "start_agent",
274
- description: "Start a web scraping agent execution. Two modes available: " +
275
- "(1) ASYNC (default): Returns immediately with runId - use get_run_status to monitor progress. " +
276
- "(2) SYNC: Set isRunSynchronously=true to wait and get scraped data directly (best for quick agents <60s). " +
277
- "Answers: 'Run agent X', 'Start the scraper', 'Execute the Amazon agent', 'Scrape this website'. " +
278
- "Returns: In async mode: {runId, status}. In sync mode: Scraped data directly as JSON/text. " +
279
- "REQUIRED: Get agentId first using list_agents or search_agents. " +
280
- "TIP: Use get_agent first to check what inputParameters the agent accepts before running.",
281
- inputSchema: {
282
- type: "object",
283
- properties: {
284
- agentId: { type: "number", description: "The unique ID of the agent to run. Get this from list_agents or search_agents." },
285
- inputParameters: { type: "string", description: "JSON string of input parameters. Check agent's inputParameters with get_agent to see what's accepted. Example: '{\"url\": \"https://example.com\"}'" },
286
- isRunSynchronously: { type: "boolean", description: "If true, wait for completion and return scraped data. If false (default), return immediately with runId. Use true only for quick agents." },
287
- timeout: { type: "number", description: "Timeout in seconds for synchronous runs. Only used when isRunSynchronously=true. Default: 60." },
288
- parallelism: { type: "number", description: "Number of parallel instances. Default: 1. Cannot be >1 when isRunSynchronously=true." },
289
- },
290
- required: ["agentId"],
291
- },
292
- annotations: {
293
- readOnlyHint: false,
294
- destructiveHint: false,
295
- },
296
- },
297
- {
298
- name: "stop_agent",
299
- description: "Stop a running agent execution immediately. Use to cancel runs that are taking too long or no longer needed. " +
300
- "Answers: 'Stop that run', 'Cancel the scraper', 'Abort agent X', 'Kill the running job'. " +
301
- "REQUIRED: You need both agentId and runId. Get runId from start_agent response or get_agent_runs.",
302
- inputSchema: {
303
- type: "object",
304
- properties: {
305
- agentId: { type: "number", description: "The unique ID of the agent." },
306
- runId: { type: "number", description: "The run ID to stop. Get this from start_agent response or get_agent_runs." },
307
- },
308
- required: ["agentId", "runId"],
309
- },
310
- annotations: {
311
- readOnlyHint: false,
312
- destructiveHint: false,
313
- },
314
- },
315
- {
316
- name: "kill_agent",
317
- description: "Force-terminate an agent when stop_agent is not working. " +
318
- "BEHAVIOR: First call initiates graceful stop (same as stop_agent). Second call forces immediate process termination if still stopping. " +
319
- "USE WHEN: stop_agent was called but agent is still running/stopping and not responding. " +
320
- "Answers: 'Force kill stuck agent', 'Agent won't stop', 'Terminate unresponsive run'. " +
321
- "REQUIRED: agentId and runId. Get runId from start_agent or get_agent_runs. " +
322
- "WARNING: This is a destructive operation that can forcefully terminate server processes.",
323
- inputSchema: {
324
- type: "object",
325
- properties: {
326
- agentId: { type: "number", description: "The unique ID of the agent." },
327
- runId: { type: "number", description: "The run ID to kill. Get from start_agent or get_agent_runs." },
328
- },
329
- required: ["agentId", "runId"],
330
- },
331
- annotations: {
332
- readOnlyHint: false,
333
- destructiveHint: true,
334
- },
335
- },
336
- {
337
- name: "delete_run",
338
- description: "Delete a run and its associated data (files, storage). Used for PII compliance. " +
339
- "Checks both active Runs and RunHistory tables automatically. " +
340
- "Answers: 'Delete run X', 'Remove run files', 'Clean up PII data from a run'. " +
341
- "WARNING: Destructive and irreversible. " +
342
- "REQUIRED: agentId and runId. Get runId from get_agent_runs.",
343
- inputSchema: {
344
- type: "object",
345
- properties: {
346
- agentId: { type: "number", description: "The ID of the agent that contains the run." },
347
- runId: { type: "number", description: "The ID of the run to delete. Get from get_agent_runs." },
348
- removeMethod: {
349
- type: "string",
350
- enum: ["RemoveEntireRun", "RemoveAllFiles", "RemoveAllFilesAndAgentInput"],
351
- description: "What to delete. " +
352
- "RemoveEntireRun (default): Completely removes the run record and all associated files. " +
353
- "RemoveAllFiles: Removes files but keeps the run record. " +
354
- "RemoveAllFilesAndAgentInput: Removes files and clears agent input parameters.",
355
- },
356
- },
357
- required: ["agentId", "runId"],
358
- },
359
- annotations: {
360
- readOnlyHint: false,
361
- destructiveHint: true,
362
- },
363
- },
364
- // File Tools
365
- {
366
- name: "get_run_files",
367
- description: "List all output files generated by a completed run. Files contain scraped data in formats like CSV, JSON, Excel. " +
368
- "Answers: 'What files did the run produce?', 'Show output files', 'Where is the scraped data?', 'Download results'. " +
369
- "Returns: Array of files with id, name, fileType, fileSize, created. " +
370
- "USE AFTER a run completes (status=Completed) to see available downloads. " +
371
- "NEXT STEP: Use get_file_download_url with a fileId to get the actual download link.",
372
- inputSchema: {
373
- type: "object",
374
- properties: {
375
- agentId: { type: "number", description: "The unique ID of the agent." },
376
- runId: { type: "number", description: "The run ID. Get this from get_agent_runs or start_agent response." },
377
- },
378
- required: ["agentId", "runId"],
379
- },
380
- annotations: {
381
- readOnlyHint: true,
382
- },
383
- },
384
- {
385
- name: "get_file_download_url",
386
- description: "Get a temporary download URL for a specific output file. The URL expires after a short time. " +
387
- "Answers: 'Download the CSV file', 'Get the output data', 'Give me the file link'. " +
388
- "Returns: Temporary URL that can be used to download the file directly. " +
389
- "REQUIRED: Get fileId first from get_run_files. " +
390
- "TIP: Share the URL with the user so they can download the file.",
391
- inputSchema: {
392
- type: "object",
393
- properties: {
394
- agentId: { type: "number", description: "The unique ID of the agent." },
395
- runId: { type: "number", description: "The run ID." },
396
- fileId: { type: "number", description: "The file ID from get_run_files response." },
397
- },
398
- required: ["agentId", "runId", "fileId"],
399
- },
400
- annotations: {
401
- readOnlyHint: true,
402
- },
403
- },
404
- // Version Tools
405
- {
406
- name: "get_agent_versions",
407
- description: "List all saved versions of an agent's configuration. Use for reviewing change history or finding a version to restore. " +
408
- "Answers: 'Show agent version history', 'What changes were made?', 'List previous versions'. " +
409
- "Returns: Array of versions with version number, userName (who made the change), created date, comments, fileSize. " +
410
- "NEXT STEP: Use restore_agent_version to roll back to a previous version if needed.",
411
- inputSchema: {
412
- type: "object",
413
- properties: {
414
- agentId: { type: "number", description: "The unique ID of the agent." },
415
- },
416
- required: ["agentId"],
417
- },
418
- annotations: {
419
- readOnlyHint: true,
420
- },
421
- },
422
- {
423
- name: "restore_agent_version",
424
- description: "Restore an agent to a previous version. This creates a NEW version based on the restored configuration. " +
425
- "Answers: 'Roll back agent to version X', 'Undo agent changes', 'Restore previous configuration'. " +
426
- "WARNING: This modifies the agent. Use get_agent_versions first to find the correct version number. " +
427
- "REQUIRED: Provide a reason in 'comments' explaining why the restore is needed.",
428
- inputSchema: {
429
- type: "object",
430
- properties: {
431
- agentId: { type: "number", description: "The unique ID of the agent." },
432
- versionNumber: { type: "number", description: "The version number to restore to. Get this from get_agent_versions." },
433
- comments: { type: "string", description: "Explanation for why this version is being restored. Will be recorded in version history." },
434
- },
435
- required: ["agentId", "versionNumber", "comments"],
436
- },
437
- annotations: {
438
- readOnlyHint: false,
439
- destructiveHint: false,
440
- },
441
- },
442
- // Schedule Tools
443
- {
444
- name: "list_agent_schedules",
445
- description: "List all scheduled tasks for a specific agent. Shows when the agent is configured to run automatically. " +
446
- "Answers: 'When does this agent run?', 'Show schedules for agent X', 'Is this agent scheduled?'. " +
447
- "Returns: Array of schedules with id, name, cronExpression/schedule, nextRunTime, isEnabled, timezone. " +
448
- "TIP: Check isEnabled to see if the schedule is active.",
449
- inputSchema: {
450
- type: "object",
451
- properties: {
452
- agentId: { type: "number", description: "The unique ID of the agent." },
453
- },
454
- required: ["agentId"],
455
- },
456
- annotations: {
457
- readOnlyHint: true,
458
- },
459
- },
460
- {
461
- name: "create_agent_schedule",
462
- description: "Create a scheduled task to automatically run an agent. " +
463
- "Three schedule types available: " +
464
- "RunOnce (1): Runs once at startTime (required, must be >=1 min in future UTC). " +
465
- "RunEvery (2): Repeats every runEveryCount periods (runEveryPeriod: 1=min, 2=hr, 3=day, 4=wk, 5=mo). Optional startTime for first run (must be in future if provided). " +
466
- "CRON (3): Uses cronExpression for complex schedules (e.g., '0 9 * * 1,4' = Mon/Thu 9am). " +
467
- "Always specify timezone for local time interpretation. " +
468
- "Examples: CRON daily at 9am: {scheduleType:3, cronExpression:'0 9 * * *'}. " +
469
- "RunOnce: {scheduleType:1, startTime:'2026-01-20T14:30:00Z'}. " +
470
- "RunEvery 30min: {scheduleType:2, runEveryCount:30, runEveryPeriod:1}.",
471
- inputSchema: {
472
- type: "object",
473
- properties: {
474
- agentId: { type: "number", description: "The unique ID of the agent to schedule. Get this from list_agents or search_agents." },
475
- name: { type: "string", description: "A descriptive name for the schedule (e.g., 'Daily Morning Run')." },
476
- scheduleType: {
477
- type: "number",
478
- enum: [1, 2, 3],
479
- description: "Schedule type: 1=RunOnce (single execution), 2=RunEvery (recurring interval), 3=CRON (cron expression). Default: 3 (CRON)."
480
- },
481
- startTime: {
482
- type: "string",
483
- description: "ISO 8601 UTC datetime (e.g., '2026-01-20T14:30:00Z'). Required for RunOnce (must be >=1 min in future). Optional for RunEvery (sets first run time, must be in future). Not used for CRON."
484
- },
485
- cronExpression: {
486
- type: "string",
487
- description: "CRON expression for scheduleType=3. Format: 'minute hour day month weekday'. Examples: '0 9 * * *' (daily 9am), '0 9 * * 1,4' (Mon/Thu 9am), '*/30 * * * *' (every 30 min)."
488
- },
489
- runEveryCount: {
490
- type: "number",
491
- description: "For scheduleType=2 (RunEvery): The interval count. Example: 30 with runEveryPeriod=1 means every 30 minutes."
492
- },
493
- runEveryPeriod: {
494
- type: "number",
495
- enum: [1, 2, 3, 4, 5],
496
- description: "For scheduleType=2 (RunEvery): The time unit. 1=minutes, 2=hours, 3=days, 4=weeks, 5=months."
497
- },
498
- timezone: {
499
- type: "string",
500
- description: "Timezone for schedule interpretation (e.g., 'America/New_York', 'America/Denver', 'Europe/London'). Default: UTC."
501
- },
502
- inputParameters: { type: "string", description: "Optional JSON string of input parameters to pass to each scheduled run." },
503
- isEnabled: { type: "boolean", description: "Whether the schedule is active. Default: true." },
504
- parallelism: { type: "number", description: "Number of parallel instances to run. Default: 1." },
505
- },
506
- required: ["agentId", "name"],
507
- },
508
- annotations: {
509
- readOnlyHint: false,
510
- destructiveHint: false,
511
- },
512
- },
513
- {
514
- name: "delete_agent_schedule",
515
- description: "Remove a schedule from an agent. The agent will no longer run automatically on this schedule. " +
516
- "Answers: 'Stop the scheduled runs', 'Remove the Monday schedule', 'Delete schedule X'. " +
517
- "WARNING: This permanently deletes the schedule. Use list_agent_schedules first to find the scheduleId.",
518
- inputSchema: {
519
- type: "object",
520
- properties: {
521
- agentId: { type: "number", description: "The unique ID of the agent." },
522
- scheduleId: { type: "number", description: "The schedule ID to delete. Get this from list_agent_schedules." },
523
- },
524
- required: ["agentId", "scheduleId"],
525
- },
526
- annotations: {
527
- readOnlyHint: false,
528
- destructiveHint: true,
529
- },
530
- },
531
- {
532
- name: "get_scheduled_runs",
533
- description: "Get all upcoming scheduled runs across all agents in a date range. Shows what will run and when. " +
534
- "Answers: 'What runs this week?', 'Show upcoming schedules', 'What agents are scheduled tomorrow?'. " +
535
- "Returns: Array of upcoming runs with scheduleId, agentId, agentName, scheduleName, nextRunTime, isEnabled. " +
536
- "TIP: If no dates provided, defaults to the next 7 days.",
537
- inputSchema: {
538
- type: "object",
539
- properties: {
540
- startDate: { type: "string", description: "Start date in ISO 8601 format. Example: '2026-01-16'. Defaults to today." },
541
- endDate: { type: "string", description: "End date in ISO 8601 format. Example: '2026-01-23'. Defaults to 7 days from start." },
542
- },
543
- required: [],
544
- },
545
- annotations: {
546
- readOnlyHint: true,
547
- },
548
- },
549
- // Billing/Credits Tools
550
- {
551
- name: "get_credits_balance",
552
- description: "Get the current available credits balance for the organization. " +
553
- "Answers: 'How many credits do I have?', 'What's my balance?', 'Check credits'. " +
554
- "Returns: availableCredits, organizationId, retrievedAt timestamp.",
555
- inputSchema: { type: "object", properties: {}, required: [] },
556
- annotations: {
557
- readOnlyHint: true,
558
- },
559
- },
560
- {
561
- name: "get_spending_summary",
562
- description: "Get a summary of credits spent in a date range. " +
563
- "Answers: 'How much have I spent?', 'What's my usage this week?', 'Show spending for January'. " +
564
- "Returns: totalSpent, startDate, endDate, currentBalance. " +
565
- "TIP: If no dates provided, returns spending for the current period.",
566
- inputSchema: {
567
- type: "object",
568
- properties: {
569
- startDate: { type: "string", description: "Start date in ISO 8601 format. Example: '2026-01-01'." },
570
- endDate: { type: "string", description: "End date in ISO 8601 format. Example: '2026-01-31'." },
571
- },
572
- required: [],
573
- },
574
- annotations: {
575
- readOnlyHint: true,
576
- },
577
- },
578
- {
579
- name: "get_credit_history",
580
- description: "Get the transaction history of credits (additions from purchases, deductions from usage). " +
581
- "Answers: 'Show credit history', 'What were my credit transactions?', 'When were credits added?'. " +
582
- "Returns: Array of transactions with transactionType, amount, balance, created date, message.",
583
- inputSchema: {
584
- type: "object",
585
- properties: {
586
- pageIndex: { type: "number", description: "Page number (1-based). Default: 1." },
587
- recordsPerPage: { type: "number", description: "Records per page. Default: 50, Max: 100." },
588
- },
589
- required: [],
590
- },
591
- annotations: {
592
- readOnlyHint: true,
593
- },
594
- },
595
- {
596
- name: "get_agents_usage",
597
- description: "Get all agents with their total costs for a date range, with filtering and sorting options. " +
598
- "USE THIS to analyze which agents are costing the most, compare agent costs, or track spending by agent. " +
599
- "Answers: 'Which agents cost the most?', 'Show agent costs this month', 'What did agent X cost in January?'. " +
600
- "Returns: Paginated list of agents with agentId, agentName, cost, spaceId, plus totalRecordCount and totalCost. " +
601
- "TIP: Use sortColumn='cost' and sortOrder=1 to see most expensive agents first. Filter by name to find specific agents. Usage types: 'Server Time,Export GB,Agent Inputs,Proxy Data,Export CPM'.",
602
- inputSchema: {
603
- type: "object",
604
- properties: {
605
- startDate: { type: "string", description: "Start date in ISO 8601 format. Defaults to start of current month. Example: '2026-01-01' or '2026-01-01T00:00:00Z'." },
606
- endDate: { type: "string", description: "End date in ISO 8601 format. Defaults to now. Example: '2026-01-31' or '2026-01-31T23:59:59Z'." },
607
- pageIndex: { type: "number", description: "Page number (1-based). Default: 1." },
608
- recordsPerPage: { type: "number", description: "Records per page. Default: 50, Max: 1000." },
609
- sortColumn: { type: "string", description: "Column to sort by: 'name' or 'cost'. Default: 'name'." },
610
- sortOrder: { type: "number", description: "Sort order: 0 = ascending, 1 = descending. Default: 0." },
611
- name: { type: "string", description: "Filter by agent name (case-insensitive contains match)." },
612
- usageTypes: { type: "string", description: "Filter by usage types (comma-separated). Example: 'Server Time,Export GB'." },
613
- },
614
- required: [],
615
- },
616
- annotations: {
617
- readOnlyHint: true,
618
- },
619
- },
620
- {
621
- name: "get_agent_cost_breakdown",
622
- description: "Get detailed cost breakdown by usage type for a specific agent over time, useful for visualizing costs in charts. " +
623
- "USE THIS to understand what's driving costs for an agent (server time vs exports vs proxies), or to chart agent costs over time. " +
624
- "Answers: 'What's causing agent X's costs?', 'Show me cost breakdown for agent 123', 'Chart agent costs by day'. " +
625
- "Returns: Cost data with agentId, agentName, date labels array, usageTypes array (each with type name, data points, totalCost), totalCost, startDate, endDate. " +
626
- "TIP: Use timeUnit='day' for daily granularity or 'month' for monthly. The labels array corresponds to data points in each usageTypes.data array.",
627
- inputSchema: {
628
- type: "object",
629
- properties: {
630
- agentId: { type: "number", description: "The unique ID of the agent." },
631
- startDate: { type: "string", description: "Start date in ISO 8601 format. Defaults to start of current month. Example: '2026-01-01' or '2026-01-01T00:00:00Z'." },
632
- endDate: { type: "string", description: "End date in ISO 8601 format. Defaults to now. Example: '2026-01-31' or '2026-01-31T23:59:59Z'." },
633
- timeUnit: { type: "string", description: "Time unit for grouping: 'day' or 'month'. Default: 'day'." },
634
- usageTypes: { type: "string", description: "Filter by usage types (comma-separated). Example: 'Server Time,Export GB'." },
635
- },
636
- required: ["agentId"],
637
- },
638
- annotations: {
639
- readOnlyHint: true,
640
- },
641
- },
642
- {
643
- name: "get_agent_runs_cost",
644
- description: "Get individual run costs for a specific agent with detailed run information and filtering options. " +
645
- "USE THIS to drill down into specific runs, identify expensive runs, or analyze run costs over time. " +
646
- "Answers: 'Which runs were most expensive?', 'Show run costs for agent X', 'What did run Y cost?'. " +
647
- "Returns: Paginated list of runs with runId, date, startTime, endTime, cost, billingType, plus agentId, agentName, totalRecordCount, totalCost. " +
648
- "TIP: Sort by cost (sortColumn='cost', sortOrder=1) to find most expensive runs. Filter by usageTypes to see specific cost categories.",
649
- inputSchema: {
650
- type: "object",
651
- properties: {
652
- agentId: { type: "number", description: "The unique ID of the agent." },
653
- startDate: { type: "string", description: "Start date in ISO 8601 format. Defaults to start of current month. Example: '2026-01-01' or '2026-01-01T00:00:00Z'." },
654
- endDate: { type: "string", description: "End date in ISO 8601 format. Defaults to now. Example: '2026-01-31' or '2026-01-31T23:59:59Z'." },
655
- pageIndex: { type: "number", description: "Page number (1-based). Default: 1." },
656
- recordsPerPage: { type: "number", description: "Records per page. Default: 50, Max: 1000." },
657
- sortColumn: { type: "string", description: "Column to sort by: 'date', 'cost', or 'duration'. Default: 'date'." },
658
- sortOrder: { type: "number", description: "Sort order: 0 = ascending, 1 = descending. Default: 0." },
659
- usageTypes: { type: "string", description: "Filter by usage types (comma-separated). Example: 'Server Time,Proxy Data'." },
660
- },
661
- required: ["agentId"],
662
- },
663
- annotations: {
664
- readOnlyHint: true,
665
- },
666
- },
667
- // Space Tools
668
- {
669
- name: "list_spaces",
670
- description: "List all accessible spaces (folders for organizing agents into groups). " +
671
- "Answers: 'What spaces do I have?', 'Show my folders', 'List agent groups'. " +
672
- "Returns: Array of spaces with id, name, description. " +
673
- "USE THIS to find spaceId before using get_space_agents or filtering list_agents by space.",
674
- inputSchema: { type: "object", properties: {}, required: [] },
675
- annotations: {
676
- readOnlyHint: true,
677
- },
678
- },
679
- {
680
- name: "get_space",
681
- description: "Get details of a specific space including its description and settings. " +
682
- "Answers: 'Tell me about space X', 'Show space details'. " +
683
- "Returns: Space details with id, name, description, organizationId, created/updated dates.",
684
- inputSchema: {
685
- type: "object",
686
- properties: {
687
- spaceId: { type: "number", description: "The unique ID of the space. Get this from list_spaces." },
688
- },
689
- required: ["spaceId"],
690
- },
691
- annotations: {
692
- readOnlyHint: true,
693
- },
694
- },
695
- {
696
- name: "get_space_agents",
697
- description: "List all agents that belong to a specific space. " +
698
- "Answers: 'What agents are in space X?', 'Show agents in the Production folder'. " +
699
- "Returns: Array of agents in the space with id, name, status, configType, lastActivity. " +
700
- "ALTERNATIVE: You can also use list_agents with spaceId filter.",
701
- inputSchema: {
702
- type: "object",
703
- properties: {
704
- spaceId: { type: "number", description: "The unique ID of the space. Get this from list_spaces or search_space_by_name." },
705
- },
706
- required: ["spaceId"],
707
- },
708
- annotations: {
709
- readOnlyHint: true,
710
- },
711
- },
712
- {
713
- name: "search_space_by_name",
714
- description: "Find a space by its name. Use when user mentions a space by name instead of ID. " +
715
- "Answers: 'Find the Production space', 'Get the Bot Blocking folder'. " +
716
- "Returns: Matching space with id, name, description. " +
717
- "NEXT STEP: Use the returned spaceId with get_space_agents or run_space_agents.",
718
- inputSchema: {
719
- type: "object",
720
- properties: {
721
- name: { type: "string", description: "The space name to search for. Case-insensitive." },
722
- },
723
- required: ["name"],
724
- },
725
- annotations: {
726
- readOnlyHint: true,
727
- },
728
- },
729
- {
730
- name: "run_space_agents",
731
- description: "Start ALL agents in a space at once (batch operation). Useful for running a group of related agents together. " +
732
- "Answers: 'Run all agents in space X', 'Execute the Production folder', 'Start all scrapers in Bot Blocking'. " +
733
- "Returns: Summary with totalAgents, agentsStarted, agentsFailed, and individual results. " +
734
- "WARNING: This starts multiple agents. Use get_space_agents first to see what will run.",
735
- inputSchema: {
736
- type: "object",
737
- properties: {
738
- spaceId: { type: "number", description: "The unique ID of the space. Get this from list_spaces or search_space_by_name." },
739
- inputParameters: { type: "string", description: "Optional JSON string of input parameters to pass to ALL agents in the space." },
740
- },
741
- required: ["spaceId"],
742
- },
743
- annotations: {
744
- readOnlyHint: false,
745
- destructiveHint: false,
746
- },
747
- },
748
- // Analytics Tools
749
- {
750
- name: "get_runs_summary",
751
- description: "Get aggregate statistics about agent runs in a date range: counts of completed, failed, running, etc. " +
752
- "Answers: 'How many agents ran yesterday?', 'What failed last week?', 'Show run statistics', 'Give me a summary of runs'. " +
753
- "Returns: totalRuns, completedRuns, failedRuns, completedWithErrorsRuns, runningRuns, queuedRuns, stoppedRuns. " +
754
- "TIP: Set includeDetails=true to get details of which specific agents failed and why. " +
755
- "TIP: Use status filter to focus on specific outcomes (e.g., 'Failed' to see only failures).",
756
- inputSchema: {
757
- type: "object",
758
- properties: {
759
- startDate: { type: "string", description: "Start date in ISO 8601 format. Example: '2026-01-15'. Defaults to today if not specified." },
760
- endDate: { type: "string", description: "End date in ISO 8601 format. Example: '2026-01-16'. Defaults to today if not specified." },
761
- status: { type: "string", description: "Filter by run status: 'Failed', 'Completed', 'CompletedWithErrors', 'Running'. Only shows runs with this status." },
762
- includeDetails: { type: "boolean", description: "If true, includes failedRunDetails array with specific agent names and error messages. Default: true." },
763
- },
764
- required: [],
765
- },
766
- annotations: {
767
- readOnlyHint: true,
768
- },
769
- },
770
- {
771
- name: "get_records_summary",
772
- description: "Get a summary of how many records were extracted and exported by agents in a date range. " +
773
- "Answers: 'How many records were scraped?', 'What was the output yesterday?', 'Show extraction statistics'. " +
774
- "Returns: totalRecordsExtracted, totalRecordsExported, totalErrors, totalPageLoads, runCount. " +
775
- "TIP: Use agentId filter to see statistics for a specific agent only.",
776
- inputSchema: {
777
- type: "object",
778
- properties: {
779
- startDate: { type: "string", description: "Start date in ISO 8601 format. Example: '2026-01-15'." },
780
- endDate: { type: "string", description: "End date in ISO 8601 format. Example: '2026-01-16'." },
781
- agentId: { type: "number", description: "Optional: Filter to show records for a specific agent only." },
782
- },
783
- required: [],
784
- },
785
- annotations: {
786
- readOnlyHint: true,
787
- },
788
- },
789
- {
790
- name: "get_run_diagnostics",
791
- description: "Get detailed diagnostics for a specific run, including error messages, possible causes, and suggested fixes. " +
792
- "Answers: 'Why did run X fail?', 'Show error details for this run', 'Debug run 123'. " +
793
- "Returns: errorMessage, possibleCauses (array), suggestedActions (array), run timing and stats. " +
794
- "USE THIS when you have a specific runId. Use get_latest_failure if you just want the most recent failure.",
795
- inputSchema: {
796
- type: "object",
797
- properties: {
798
- agentId: { type: "number", description: "The unique ID of the agent." },
799
- runId: { type: "number", description: "The run ID to diagnose. Get this from get_agent_runs." },
800
- },
801
- required: ["agentId", "runId"],
802
- },
803
- annotations: {
804
- readOnlyHint: true,
805
- },
806
- },
807
- {
808
- name: "get_latest_failure",
809
- description: "Get diagnostics for the most recent failed run of an agent. Includes error analysis and suggested fixes. " +
810
- "Answers: 'Why did my agent fail?', 'What went wrong?', 'Debug agent X', 'Show the last error'. " +
811
- "Returns: errorMessage, possibleCauses, suggestedActions, run timing and stats. " +
812
- "USE THIS instead of get_run_diagnostics when user asks about failure without specifying a run ID. " +
813
- "SHORTCUT: This is faster than calling get_agent_runs + filtering for Failed + get_run_diagnostics.",
814
- inputSchema: {
815
- type: "object",
816
- properties: {
817
- agentId: { type: "number", description: "The unique ID of the agent. Get this from list_agents or search_agents." },
818
- },
819
- required: ["agentId"],
820
- },
821
- annotations: {
822
- readOnlyHint: true,
823
- },
824
- },
825
- ];
826
- // ==========================================
827
- // Server Factory
828
- // ==========================================
829
- /**
830
- * Create a new MCP Server instance with all handlers registered.
831
- * Each session in HTTP mode needs its own Server instance.
832
- *
833
- * @param apiClient - The API client to use for this server instance
834
- * @returns Configured MCP Server instance
835
- */
836
- function createMcpServer(apiClient) {
837
- const server = new Server({
838
- name: "sequentum-mcp-server",
839
- version,
840
- }, {
841
- capabilities: {
842
- tools: {},
843
- },
844
- });
845
- // Handle tool listing
846
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
847
- tools,
848
- }));
849
- // Handle tool execution
850
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
851
- const { name, arguments: args } = request.params;
852
- if (DEBUG) {
853
- console.error(`[DEBUG] Tool called: ${name}`);
854
- console.error(`[DEBUG] Args: ${JSON.stringify(args)}`);
855
- }
856
- try {
857
- switch (name) {
858
- // Agent Tools
859
- case "list_agents": {
860
- const params = args;
861
- const statusNum = validateNumber(params, "status", false);
862
- const spaceId = validateNumber(params, "spaceId", false);
863
- const search = validateString(params, "search", false);
864
- const configTypeStr = validateEnum(params, "configType", ["Agent", "Command", "Api", "Shared"], false);
865
- const sortColumn = validateEnum(params, "sortColumn", ["name", "lastActivity", "created", "updated", "status", "configType"], false);
866
- const sortOrderStr = validateEnum(params, "sortOrder", ["asc", "desc"], false);
867
- const pageIndex = validateNumber(params, "pageIndex", false);
868
- const recordsPerPage = validateNumber(params, "recordsPerPage", false);
869
- // Build filters object - ALWAYS include pagination to ensure resource-efficient API calls
870
- const filters = {
871
- // Always enforce pagination with defaults (pageIndex is 1-based per API spec)
872
- pageIndex: pageIndex ?? 1,
873
- recordsPerPage: recordsPerPage ?? 50,
874
- };
875
- // Add other optional filters
876
- // Status is now the RunStatus enum value (1=Running, 7=Failed, 9=Completed, etc.)
877
- if (statusNum !== undefined) {
878
- filters.status = statusNum;
879
- }
880
- if (spaceId !== undefined) {
881
- filters.spaceId = spaceId;
882
- }
883
- if (search) {
884
- filters.search = search;
885
- }
886
- if (configTypeStr) {
887
- filters.configType = configTypeStr;
888
- }
889
- if (sortColumn) {
890
- filters.sortColumn = sortColumn;
891
- }
892
- if (sortOrderStr) {
893
- // Convert "asc"/"desc" to 0/1 as the API expects
894
- filters.sortOrder = sortOrderStr === "desc" ? 1 : 0;
895
- }
896
- if (DEBUG) {
897
- console.error(`[DEBUG] Calling getAllAgents with filters: ${JSON.stringify(filters)}`);
898
- }
899
- const response = await apiClient.getAllAgents(filters);
900
- if (DEBUG) {
901
- console.error(`[DEBUG] Response type: ${typeof response}, isArray: ${Array.isArray(response)}`);
902
- }
903
- // Check if response is paginated (has 'data', 'items', 'records', or 'agents' property) or plain array
904
- let agents;
905
- let paginationInfo = null;
906
- if (Array.isArray(response)) {
907
- agents = response;
908
- }
909
- else if (response && typeof response === 'object') {
910
- // Try common property names for paginated responses
911
- const possibleDataProps = ['data', 'items', 'records', 'agents', 'results'];
912
- const dataKey = possibleDataProps.find(key => key in response && Array.isArray(response[key]));
913
- if (dataKey) {
914
- agents = response[dataKey];
915
- paginationInfo = {
916
- totalCount: response.totalCount ?? response.total ?? response.count,
917
- pageIndex: response.pageIndex ?? response.page,
918
- recordsPerPage: response.recordsPerPage ?? response.pageSize ?? response.limit,
919
- };
920
- }
921
- else {
922
- // Unknown structure - return raw response
923
- if (DEBUG) {
924
- console.error(`[DEBUG] Unknown response structure: ${JSON.stringify(response).substring(0, 500)}`);
925
- }
926
- return {
927
- content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
928
- };
929
- }
930
- }
931
- else {
932
- throw new Error(`Unexpected response type: ${typeof response}`);
933
- }
934
- if (DEBUG) {
935
- console.error(`[DEBUG] getAllAgents returned ${agents.length} agents`);
936
- }
937
- const summary = summarizeAgents(agents);
938
- // Include pagination info if available
939
- const result = paginationInfo ? {
940
- agents: summary,
941
- pagination: paginationInfo,
942
- } : summary;
943
- return {
944
- content: [
945
- {
946
- type: "text",
947
- text: JSON.stringify(result, null, 2),
948
- },
949
- ],
950
- };
951
- }
952
- case "get_agent": {
953
- const params = args;
954
- const agentId = validateNumber(params, "agentId");
955
- const agent = await apiClient.getAgent(agentId);
956
- return {
957
- content: [
958
- {
959
- type: "text",
960
- text: JSON.stringify(agent, null, 2),
961
- },
962
- ],
963
- };
964
- }
965
- case "search_agents": {
966
- const params = args;
967
- const query = validateString(params, "query");
968
- if (!query.trim()) {
969
- throw new Error("Search query cannot be empty");
970
- }
971
- const maxRecords = validateNumber(params, "maxRecords", false);
972
- const agents = await apiClient.searchAgents(query, maxRecords);
973
- return {
974
- content: [
975
- {
976
- type: "text",
977
- text: JSON.stringify(summarizeAgents(agents), null, 2),
978
- },
979
- ],
980
- };
981
- }
982
- // Run Tools
983
- case "get_agent_runs": {
984
- const params = args;
985
- const agentId = validateNumber(params, "agentId");
986
- const maxRecords = validateNumber(params, "maxRecords", false);
987
- const runs = await apiClient.getAgentRuns(agentId, maxRecords);
988
- return {
989
- content: [
990
- {
991
- type: "text",
992
- text: JSON.stringify(runs, null, 2),
993
- },
994
- ],
995
- };
996
- }
997
- case "get_run_status": {
998
- const params = args;
999
- const agentId = validateNumber(params, "agentId");
1000
- const runId = validateNumber(params, "runId");
1001
- const status = await apiClient.getRunStatus(agentId, runId);
1002
- return {
1003
- content: [
1004
- {
1005
- type: "text",
1006
- text: JSON.stringify(status, null, 2),
1007
- },
1008
- ],
1009
- };
1010
- }
1011
- case "start_agent": {
1012
- const params = args;
1013
- const agentId = validateNumber(params, "agentId");
1014
- const inputParameters = validateString(params, "inputParameters", false);
1015
- const isRunSynchronously = validateBoolean(params, "isRunSynchronously", false);
1016
- const timeout = validateNumber(params, "timeout", false);
1017
- const parallelism = validateNumber(params, "parallelism", false);
1018
- const result = await apiClient.startAgent(agentId, {
1019
- inputParameters,
1020
- isRunSynchronously: isRunSynchronously ?? false,
1021
- timeout: timeout ?? 60,
1022
- parallelism: parallelism ?? 1,
1023
- });
1024
- if (typeof result === "string") {
1025
- // Synchronous run returned data directly
1026
- return {
1027
- content: [
1028
- {
1029
- type: "text",
1030
- text: result,
1031
- },
1032
- ],
1033
- };
1034
- }
1035
- else {
1036
- // Asynchronous run returned run info
1037
- return {
1038
- content: [
1039
- {
1040
- type: "text",
1041
- text: `Agent started successfully.\n\n${JSON.stringify(result, null, 2)}`,
1042
- },
1043
- ],
1044
- };
1045
- }
1046
- }
1047
- case "stop_agent": {
1048
- const params = args;
1049
- const agentId = validateNumber(params, "agentId");
1050
- const runId = validateNumber(params, "runId");
1051
- await apiClient.stopAgent(agentId, runId);
1052
- return {
1053
- content: [
1054
- {
1055
- type: "text",
1056
- text: `Successfully stopped run ${runId} for agent ${agentId}`,
1057
- },
1058
- ],
1059
- };
1060
- }
1061
- case "kill_agent": {
1062
- const params = args;
1063
- const agentId = validateNumber(params, "agentId");
1064
- const runId = validateNumber(params, "runId");
1065
- await client.killAgent(agentId, runId);
1066
- return {
1067
- content: [
1068
- {
1069
- type: "text",
1070
- text: `Kill command sent for run ${runId} of agent ${agentId}. If the agent was running, it will initiate graceful stop. If already stopping, it will force immediate termination.`,
1071
- },
1072
- ],
1073
- };
1074
- }
1075
- // Destructive Operations
1076
- case "delete_run": {
1077
- const params = args;
1078
- const agentId = validateNumber(params, "agentId");
1079
- const runId = validateNumber(params, "runId");
1080
- const removeMethod = validateEnum(params, "removeMethod", [
1081
- "RemoveEntireRun",
1082
- "RemoveAllFiles",
1083
- "RemoveAllFilesAndAgentInput",
1084
- ], false);
1085
- await apiClient.deleteRun(agentId, runId, removeMethod);
1086
- const methodDescriptions = {
1087
- RemoveEntireRun: `Successfully deleted run ${runId} and all associated files from agent ${agentId}.`,
1088
- RemoveAllFiles: `Successfully removed all files for run ${runId} from agent ${agentId}. The run record has been preserved.`,
1089
- RemoveAllFilesAndAgentInput: `Successfully removed all files and agent input for run ${runId} from agent ${agentId}. The run record has been preserved.`,
1090
- };
1091
- const description = methodDescriptions[removeMethod ?? "RemoveEntireRun"] ??
1092
- `Operation completed for run ${runId} on agent ${agentId} (method: ${removeMethod ?? "RemoveEntireRun"})`;
1093
- return {
1094
- content: [
1095
- {
1096
- type: "text",
1097
- text: description,
1098
- },
1099
- ],
1100
- };
1101
- }
1102
- // File Tools
1103
- case "get_run_files": {
1104
- const params = args;
1105
- const agentId = validateNumber(params, "agentId");
1106
- const runId = validateNumber(params, "runId");
1107
- const files = await apiClient.getRunFiles(agentId, runId);
1108
- if (files.length === 0) {
1109
- return {
1110
- content: [
1111
- {
1112
- type: "text",
1113
- text: "No files found for this run.",
1114
- },
1115
- ],
1116
- };
1117
- }
1118
- const summary = files.map((f) => ({
1119
- id: f.id,
1120
- name: f.name,
1121
- fileType: f.fileType,
1122
- fileSize: `${((f.fileSize ?? 0) / 1024).toFixed(2)} KB`,
1123
- created: f.created,
1124
- }));
1125
- return {
1126
- content: [
1127
- {
1128
- type: "text",
1129
- text: JSON.stringify(summary, null, 2),
1130
- },
1131
- ],
1132
- };
1133
- }
1134
- case "get_file_download_url": {
1135
- const params = args;
1136
- const agentId = validateNumber(params, "agentId");
1137
- const runId = validateNumber(params, "runId");
1138
- const fileId = validateNumber(params, "fileId");
1139
- const result = await apiClient.downloadRunFile(agentId, runId, fileId);
1140
- return {
1141
- content: [
1142
- {
1143
- type: "text",
1144
- text: `Download URL:\n${result.redirectUrl}\n\nNote: This URL is temporary and will expire.`,
1145
- },
1146
- ],
1147
- };
1148
- }
1149
- // Version Tools
1150
- case "get_agent_versions": {
1151
- const params = args;
1152
- const agentId = validateNumber(params, "agentId");
1153
- const versions = await apiClient.getAgentVersions(agentId);
1154
- return {
1155
- content: [
1156
- {
1157
- type: "text",
1158
- text: JSON.stringify(versions, null, 2),
1159
- },
1160
- ],
1161
- };
1162
- }
1163
- case "restore_agent_version": {
1164
- const params = args;
1165
- const agentId = validateNumber(params, "agentId");
1166
- const versionNumber = validateNumber(params, "versionNumber");
1167
- const comments = validateString(params, "comments");
1168
- await apiClient.restoreAgentVersion(agentId, versionNumber, comments);
1169
- return {
1170
- content: [
1171
- {
1172
- type: "text",
1173
- text: `Successfully restored agent ${agentId} to version ${versionNumber}.\n\nA new version has been created based on version ${versionNumber}.`,
1174
- },
1175
- ],
1176
- };
1177
- }
1178
- // Schedule Tools
1179
- case "list_agent_schedules": {
1180
- const params = args;
1181
- const agentId = validateNumber(params, "agentId");
1182
- const schedules = await apiClient.getAgentSchedules(agentId);
1183
- return {
1184
- content: [
1185
- {
1186
- type: "text",
1187
- text: JSON.stringify(schedules, null, 2),
1188
- },
1189
- ],
1190
- };
1191
- }
1192
- case "create_agent_schedule": {
1193
- const params = args;
1194
- const agentId = validateNumber(params, "agentId");
1195
- const name = validateString(params, "name");
1196
- const scheduleType = validateNumber(params, "scheduleType", false);
1197
- const startTime = validateString(params, "startTime", false);
1198
- const cronExpression = validateString(params, "cronExpression", false);
1199
- const runEveryCount = validateNumber(params, "runEveryCount", false);
1200
- const runEveryPeriod = validateNumber(params, "runEveryPeriod", false);
1201
- const timezone = validateString(params, "timezone", false);
1202
- const inputParameters = validateString(params, "inputParameters", false);
1203
- const isEnabled = validateBoolean(params, "isEnabled", false);
1204
- const parallelism = validateNumber(params, "parallelism", false);
1205
- // Validate schedule type specific parameters
1206
- const effectiveScheduleType = scheduleType ?? 3; // Default to CRON
1207
- // RunOnce (1): startTime is required and must be at least 1 minute in the future
1208
- if (effectiveScheduleType === 1) {
1209
- if (!startTime) {
1210
- throw new Error("startTime is required when scheduleType is 1 (RunOnce)");
1211
- }
1212
- validateStartTimeInFuture(startTime, 1);
1213
- }
1214
- // RunEvery (2): runEveryCount and runEveryPeriod are required, startTime is optional but must be in the future if provided
1215
- if (effectiveScheduleType === 2) {
1216
- if (runEveryCount === undefined || runEveryPeriod === undefined) {
1217
- throw new Error("runEveryCount and runEveryPeriod are required when scheduleType is 2 (RunEvery)");
1218
- }
1219
- if (startTime) {
1220
- validateStartTimeInFuture(startTime, 0);
1221
- }
1222
- }
1223
- // CRON (3): cronExpression is required, startTime is not used
1224
- if (effectiveScheduleType === 3 && !cronExpression) {
1225
- throw new Error("cronExpression is required when scheduleType is 3 (CRON)");
1226
- }
1227
- const schedule = await apiClient.createAgentSchedule(agentId, {
1228
- name,
1229
- scheduleType: effectiveScheduleType,
1230
- startTime,
1231
- cronExpression,
1232
- runEveryCount,
1233
- runEveryPeriod,
1234
- timezone,
1235
- inputParameters,
1236
- isEnabled: isEnabled ?? true,
1237
- parallelism: parallelism ?? 1,
1238
- });
1239
- return {
1240
- content: [
1241
- {
1242
- type: "text",
1243
- text: `Schedule created successfully.\n\n${JSON.stringify(schedule, null, 2)}`,
1244
- },
1245
- ],
1246
- };
1247
- }
1248
- case "delete_agent_schedule": {
1249
- const params = args;
1250
- const agentId = validateNumber(params, "agentId");
1251
- const scheduleId = validateNumber(params, "scheduleId");
1252
- await apiClient.deleteAgentSchedule(agentId, scheduleId);
1253
- return {
1254
- content: [
1255
- {
1256
- type: "text",
1257
- text: `Successfully deleted schedule ${scheduleId} from agent ${agentId}`,
1258
- },
1259
- ],
1260
- };
1261
- }
1262
- case "get_scheduled_runs": {
1263
- const params = args;
1264
- const startDate = validateString(params, "startDate", false);
1265
- const endDate = validateString(params, "endDate", false);
1266
- const schedules = await apiClient.getUpcomingSchedules(startDate, endDate);
1267
- return {
1268
- content: [
1269
- {
1270
- type: "text",
1271
- text: JSON.stringify(schedules, null, 2),
1272
- },
1273
- ],
1274
- };
1275
- }
1276
- // Billing/Credits Tools
1277
- case "get_credits_balance": {
1278
- const balance = await apiClient.getCreditsBalance();
1279
- return {
1280
- content: [
1281
- {
1282
- type: "text",
1283
- text: JSON.stringify(balance, null, 2),
1284
- },
1285
- ],
1286
- };
1287
- }
1288
- case "get_spending_summary": {
1289
- const params = args;
1290
- const startDate = validateString(params, "startDate", false);
1291
- const endDate = validateString(params, "endDate", false);
1292
- const spending = await apiClient.getSpendingSummary(startDate, endDate);
1293
- return {
1294
- content: [
1295
- {
1296
- type: "text",
1297
- text: JSON.stringify(spending, null, 2),
1298
- },
1299
- ],
1300
- };
1301
- }
1302
- case "get_credit_history": {
1303
- const params = args;
1304
- const pageIndex = validateNumber(params, "pageIndex", false);
1305
- const recordsPerPage = validateNumber(params, "recordsPerPage", false);
1306
- const history = await apiClient.getCreditHistory(pageIndex, recordsPerPage);
1307
- return {
1308
- content: [
1309
- {
1310
- type: "text",
1311
- text: JSON.stringify(history, null, 2),
1312
- },
1313
- ],
1314
- };
1315
- }
1316
- case "get_agents_usage": {
1317
- const params = args;
1318
- const defaults = getDefaultDateRange();
1319
- const startDate = validateString(params, "startDate", false) ?? defaults.startDate;
1320
- const endDate = validateString(params, "endDate", false) ?? defaults.endDate;
1321
- validateISODate(startDate, "startDate");
1322
- validateISODate(endDate, "endDate");
1323
- validateDateRange(startDate, endDate);
1324
- const pageIndex = validateNumber(params, "pageIndex", false);
1325
- const recordsPerPage = validateNumber(params, "recordsPerPage", false);
1326
- const sortColumn = validateString(params, "sortColumn", false);
1327
- const sortOrder = validateNumber(params, "sortOrder", false);
1328
- const name = validateString(params, "name", false);
1329
- const usageTypes = validateString(params, "usageTypes", false);
1330
- const result = await apiClient.getAgentsUsage(startDate, endDate, pageIndex, recordsPerPage, sortColumn, sortOrder, name, usageTypes);
1331
- return {
1332
- content: [
1333
- {
1334
- type: "text",
1335
- text: JSON.stringify(result, null, 2),
1336
- },
1337
- ],
1338
- };
1339
- }
1340
- case "get_agent_cost_breakdown": {
1341
- const params = args;
1342
- const agentId = validateNumber(params, "agentId");
1343
- const defaults = getDefaultDateRange();
1344
- const startDate = validateString(params, "startDate", false) ?? defaults.startDate;
1345
- const endDate = validateString(params, "endDate", false) ?? defaults.endDate;
1346
- validateISODate(startDate, "startDate");
1347
- validateISODate(endDate, "endDate");
1348
- validateDateRange(startDate, endDate);
1349
- const timeUnit = validateString(params, "timeUnit", false);
1350
- const usageTypes = validateString(params, "usageTypes", false);
1351
- const result = await apiClient.getAgentCostBreakdown(agentId, startDate, endDate, timeUnit, usageTypes);
1352
- return {
1353
- content: [
1354
- {
1355
- type: "text",
1356
- text: JSON.stringify(result, null, 2),
1357
- },
1358
- ],
1359
- };
1360
- }
1361
- case "get_agent_runs_cost": {
1362
- const params = args;
1363
- const agentId = validateNumber(params, "agentId");
1364
- const defaults = getDefaultDateRange();
1365
- const startDate = validateString(params, "startDate", false) ?? defaults.startDate;
1366
- const endDate = validateString(params, "endDate", false) ?? defaults.endDate;
1367
- validateISODate(startDate, "startDate");
1368
- validateISODate(endDate, "endDate");
1369
- validateDateRange(startDate, endDate);
1370
- const pageIndex = validateNumber(params, "pageIndex", false);
1371
- const recordsPerPage = validateNumber(params, "recordsPerPage", false);
1372
- const sortColumn = validateString(params, "sortColumn", false);
1373
- const sortOrder = validateNumber(params, "sortOrder", false);
1374
- const usageTypes = validateString(params, "usageTypes", false);
1375
- const result = await apiClient.getAgentRunsCost(agentId, startDate, endDate, pageIndex, recordsPerPage, sortColumn, sortOrder, usageTypes);
1376
- return {
1377
- content: [
1378
- {
1379
- type: "text",
1380
- text: JSON.stringify(result, null, 2),
1381
- },
1382
- ],
1383
- };
1384
- }
1385
- // Space Tools
1386
- case "list_spaces": {
1387
- const spaces = await apiClient.getAllSpaces();
1388
- return {
1389
- content: [
1390
- {
1391
- type: "text",
1392
- text: JSON.stringify(spaces, null, 2),
1393
- },
1394
- ],
1395
- };
1396
- }
1397
- case "get_space": {
1398
- const params = args;
1399
- const spaceId = validateNumber(params, "spaceId");
1400
- const space = await apiClient.getSpace(spaceId);
1401
- return {
1402
- content: [
1403
- {
1404
- type: "text",
1405
- text: JSON.stringify(space, null, 2),
1406
- },
1407
- ],
1408
- };
1409
- }
1410
- case "get_space_agents": {
1411
- const params = args;
1412
- const spaceId = validateNumber(params, "spaceId");
1413
- const agents = await apiClient.getSpaceAgents(spaceId);
1414
- return {
1415
- content: [
1416
- {
1417
- type: "text",
1418
- text: JSON.stringify(agents, null, 2),
1419
- },
1420
- ],
1421
- };
1422
- }
1423
- case "search_space_by_name": {
1424
- const params = args;
1425
- const name = validateString(params, "name");
1426
- const space = await apiClient.searchSpaceByName(name);
1427
- return {
1428
- content: [
1429
- {
1430
- type: "text",
1431
- text: JSON.stringify(space, null, 2),
1432
- },
1433
- ],
1434
- };
1435
- }
1436
- case "run_space_agents": {
1437
- const params = args;
1438
- const spaceId = validateNumber(params, "spaceId");
1439
- const inputParameters = validateString(params, "inputParameters", false);
1440
- const result = await apiClient.runSpaceAgents(spaceId, inputParameters);
1441
- return {
1442
- content: [
1443
- {
1444
- type: "text",
1445
- text: `Started agents in space.\n\n${JSON.stringify(result, null, 2)}`,
1446
- },
1447
- ],
1448
- };
1449
- }
1450
- // Analytics Tools
1451
- case "get_runs_summary": {
1452
- const params = args;
1453
- const startDate = validateString(params, "startDate", false);
1454
- const endDate = validateString(params, "endDate", false);
1455
- const status = validateString(params, "status", false);
1456
- const includeDetails = validateBoolean(params, "includeDetails", false);
1457
- const summary = await apiClient.getRunsSummary(startDate, endDate, status, includeDetails);
1458
- return {
1459
- content: [
1460
- {
1461
- type: "text",
1462
- text: JSON.stringify(summary, null, 2),
1463
- },
1464
- ],
1465
- };
1466
- }
1467
- case "get_records_summary": {
1468
- const params = args;
1469
- const startDate = validateString(params, "startDate", false);
1470
- const endDate = validateString(params, "endDate", false);
1471
- const agentId = validateNumber(params, "agentId", false);
1472
- const summary = await apiClient.getRecordsSummary(startDate, endDate, agentId);
1473
- return {
1474
- content: [
1475
- {
1476
- type: "text",
1477
- text: JSON.stringify(summary, null, 2),
1478
- },
1479
- ],
1480
- };
1481
- }
1482
- case "get_run_diagnostics": {
1483
- const params = args;
1484
- const agentId = validateNumber(params, "agentId");
1485
- const runId = validateNumber(params, "runId");
1486
- const diagnostics = await apiClient.getRunDiagnostics(agentId, runId);
1487
- return {
1488
- content: [
1489
- {
1490
- type: "text",
1491
- text: JSON.stringify(diagnostics, null, 2),
1492
- },
1493
- ],
1494
- };
1495
- }
1496
- case "get_latest_failure": {
1497
- const params = args;
1498
- const agentId = validateNumber(params, "agentId");
1499
- const diagnostics = await apiClient.getLatestFailure(agentId);
1500
- return {
1501
- content: [
1502
- {
1503
- type: "text",
1504
- text: JSON.stringify(diagnostics, null, 2),
1505
- },
1506
- ],
1507
- };
1508
- }
1509
- default:
1510
- throw new Error(`Unknown tool: ${name}`);
1511
- }
1512
- }
1513
- catch (error) {
1514
- const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
1515
- return {
1516
- content: [
1517
- {
1518
- type: "text",
1519
- text: `Error: ${errorMessage}`,
1520
- },
1521
- ],
1522
- isError: true,
1523
- };
1524
- }
1525
- });
1526
- return server;
1527
- }
1528
81
  // ==========================================
1529
82
  // Main Entry Point
1530
83
  // ==========================================
@@ -1532,296 +85,17 @@ function createMcpServer(apiClient) {
1532
85
  * Start the MCP server in stdio mode (for Claude Code and local development)
1533
86
  */
1534
87
  async function startStdioServer() {
1535
- console.error(`Authentication: API Key`);
1536
- // Create server instance and connect to stdio transport
1537
- const server = createMcpServer(client);
88
+ console.error("Authentication: API Key");
89
+ const client = new SequentumApiClient(API_BASE_URL, API_KEY);
90
+ const server = createMcpServer(client, version);
1538
91
  const transport = new StdioServerTransport();
1539
92
  await server.connect(transport);
1540
93
  console.error("Sequentum MCP Server running on stdio");
1541
94
  console.error(`Connected to: ${API_BASE_URL}`);
1542
95
  }
1543
- /**
1544
- * Start the MCP server in HTTP mode (for Claude Connectors)
1545
- * Uses Streamable HTTP transport as required by Claude Connectors Directory
1546
- * Creates a new Server instance per session for proper isolation.
1547
- */
1548
- async function startHttpServer() {
1549
- const app = express();
1550
- // Trust X-Forwarded-Proto from reverse proxies (cloudflared, ngrok, etc.)
1551
- // This ensures req.protocol returns 'https' when behind a TLS-terminating proxy
1552
- // WARNING: Only safe if direct access to this server is blocked at the network level
1553
- app.set('trust proxy', process.env.TRUST_PROXY !== 'false');
1554
- // Parse JSON bodies
1555
- app.use(express.json());
1556
- // CORS middleware - required for browser-based clients like MCP Inspector
1557
- app.use((req, res, next) => {
1558
- res.setHeader("Access-Control-Allow-Origin", "*");
1559
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1560
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, mcp-session-id");
1561
- res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
1562
- // Handle preflight requests
1563
- if (req.method === "OPTIONS") {
1564
- res.status(204).end();
1565
- return;
1566
- }
1567
- next();
1568
- });
1569
- // Store sessions by session ID - each session has its own server, transport, and API client
1570
- const sessions = new Map();
1571
- // Clean up stale sessions every 15 minutes
1572
- // Sessions are removed if they haven't had activity in over 1 hour
1573
- const SESSION_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
1574
- const SESSION_CLEANUP_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
1575
- setInterval(() => {
1576
- const now = Date.now();
1577
- let cleanedCount = 0;
1578
- for (const [sessionId, session] of sessions) {
1579
- if (now - session.lastActivityAt > SESSION_MAX_AGE_MS) {
1580
- sessions.delete(sessionId);
1581
- cleanedCount++;
1582
- }
1583
- }
1584
- if (cleanedCount > 0 || DEBUG) {
1585
- console.error(`[MCP] Session cleanup: removed ${cleanedCount} stale sessions, ${sessions.size} active`);
1586
- }
1587
- }, SESSION_CLEANUP_INTERVAL_MS);
1588
- /**
1589
- * Create a new HTTP session with MCP server, transport, and API client
1590
- */
1591
- async function createSession(token) {
1592
- const sessionApiClient = new SequentumApiClient(API_BASE_URL, null);
1593
- if (token) {
1594
- sessionApiClient.setAccessToken(token);
1595
- }
1596
- const server = createMcpServer(sessionApiClient);
1597
- const transport = new StreamableHTTPServerTransport({
1598
- sessionIdGenerator: () => randomUUID(),
1599
- });
1600
- await server.connect(transport);
1601
- const now = Date.now();
1602
- return {
1603
- server,
1604
- transport,
1605
- apiClient: sessionApiClient,
1606
- createdAt: now,
1607
- lastActivityAt: now
1608
- };
1609
- }
1610
- // Health check endpoint
1611
- app.get("/health", (_req, res) => {
1612
- res.json({ status: "ok", version, transport: "streamable-http" });
1613
- });
1614
- // OAuth2 Authorization Server Metadata (RFC 8414)
1615
- // This enables MCP clients to discover OAuth2 endpoints automatically
1616
- // OAuth URLs are derived from the API base URL (same server hosts both API and OAuth)
1617
- // RFC 8414 standard path - Authorization Server Metadata
1618
- app.get("/.well-known/oauth-authorization-server", (_req, res) => {
1619
- const metadata = buildOAuthMetadata(API_BASE_URL);
1620
- res.json(metadata);
1621
- });
1622
- // RFC 9728 - Protected Resource Metadata (required by MCP spec 2025-06-18)
1623
- // This tells MCP clients which authorization server to use for this resource.
1624
- // Per MCP spec, the resource MUST be the MCP server's own canonical URL,
1625
- // as MCP clients compute the expected resource from the URL they connect to.
1626
- app.get("/.well-known/oauth-protected-resource", async (req, res) => {
1627
- // The resource is this MCP server's own URL (origin)
1628
- // MCP clients (e.g., Cursor) validate this matches the URL they connected to
1629
- const resourceUrl = new URL(`${req.protocol}://${req.get("host")}`).origin;
1630
- const protectedResourceMetadata = {
1631
- // The canonical URI of this MCP server (the protected resource)
1632
- resource: resourceUrl,
1633
- // Authorization servers that can issue tokens for this resource
1634
- authorization_servers: [API_BASE_URL],
1635
- // Scopes supported by this resource
1636
- scopes_supported: [...SUPPORTED_SCOPES],
1637
- // Bearer token is required
1638
- bearer_methods_supported: ["header"],
1639
- };
1640
- res.json(protectedResourceMetadata);
1641
- });
1642
- // Log incoming requests for debugging (only when DEBUG is enabled)
1643
- if (DEBUG) {
1644
- app.use("/mcp", (req, _res, next) => {
1645
- console.error(`[MCP] ${req.method} ${req.url}`);
1646
- // Redact sensitive headers before logging
1647
- const safeHeaders = { ...req.headers };
1648
- const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key'];
1649
- for (const header of sensitiveHeaders) {
1650
- if (safeHeaders[header]) {
1651
- safeHeaders[header] = '[REDACTED]';
1652
- }
1653
- }
1654
- console.error(`[MCP] Headers: ${JSON.stringify(safeHeaders)}`);
1655
- if (req.body && Object.keys(req.body).length > 0) {
1656
- console.error(`[MCP] Body: ${JSON.stringify(req.body)}`);
1657
- }
1658
- next();
1659
- });
1660
- }
1661
- // Handle POST requests for client-to-server messages
1662
- app.post("/mcp", async (req, res) => {
1663
- try {
1664
- // Get session ID from header
1665
- const sessionId = req.headers["mcp-session-id"];
1666
- let session;
1667
- if (sessionId) {
1668
- session = sessions.get(sessionId);
1669
- }
1670
- // If no existing session, create a new one
1671
- if (!session) {
1672
- // Extract Bearer token from Authorization header
1673
- const authHeader = req.headers.authorization;
1674
- let token = null;
1675
- if (authHeader && authHeader.startsWith("Bearer ")) {
1676
- token = authHeader.substring(7);
1677
- if (DEBUG) {
1678
- console.error("[DEBUG] Bearer token received for new session");
1679
- }
1680
- }
1681
- // Require authentication for new sessions (unless REQUIRE_AUTH=false for testing)
1682
- const requireAuth = process.env.REQUIRE_AUTH !== "false";
1683
- if (requireAuth && !token) {
1684
- // Return 401 with WWW-Authenticate header per RFC 9728 Section 5.1
1685
- // The resource is this MCP server's own URL (the protected resource)
1686
- const mcpServerUrl = new URL(`${req.protocol}://${req.get("host")}`).origin;
1687
- const wwwAuth = `Bearer resource="${mcpServerUrl}"`;
1688
- const prmUrl = `${mcpServerUrl}/.well-known/oauth-protected-resource`;
1689
- res.setHeader("WWW-Authenticate", wwwAuth);
1690
- const responseBody = {
1691
- jsonrpc: "2.0",
1692
- error: {
1693
- code: -32001,
1694
- message: "Authentication required",
1695
- data: {
1696
- // Point to Protected Resource Metadata on this MCP server
1697
- protectedResourceMetadata: prmUrl,
1698
- },
1699
- },
1700
- id: null,
1701
- };
1702
- res.status(401).json(responseBody);
1703
- console.error("[MCP] 401 - Authentication required, no Bearer token provided");
1704
- return;
1705
- }
1706
- // Create session with MCP server, transport, and API client
1707
- session = await createSession(token);
1708
- // We'll store the session after handleRequest sets the session ID
1709
- }
1710
- // Update last activity timestamp for session keep-alive
1711
- session.lastActivityAt = Date.now();
1712
- // Update token if provided (in case of token refresh)
1713
- const authHeader = req.headers.authorization;
1714
- if (authHeader && authHeader.startsWith("Bearer ")) {
1715
- const token = authHeader.substring(7);
1716
- session.apiClient.setAccessToken(token);
1717
- }
1718
- // Handle the request
1719
- await session.transport.handleRequest(req, res, req.body);
1720
- // Store session if it's new (get session ID from response header)
1721
- if (!sessionId) {
1722
- const newSessionId = res.getHeader("mcp-session-id");
1723
- if (newSessionId && !sessions.has(newSessionId)) {
1724
- sessions.set(newSessionId, session);
1725
- }
1726
- else if (!newSessionId) {
1727
- // Cleanup orphaned session to prevent memory leaks
1728
- // This can happen if handleRequest fails to set a session ID
1729
- console.error(`[MCP] Warning: No session ID returned, cleaning up orphaned session`);
1730
- try {
1731
- await session.server.close();
1732
- }
1733
- catch (closeError) {
1734
- console.error(`[MCP] Error closing orphaned session:`, closeError);
1735
- }
1736
- }
1737
- }
1738
- }
1739
- catch (error) {
1740
- console.error("Error handling MCP POST request:", error);
1741
- if (!res.headersSent) {
1742
- // Sanitize error messages in production to avoid exposing internal details
1743
- const errorMessage = DEBUG
1744
- ? (error instanceof Error ? error.message : "Internal server error")
1745
- : "Internal server error";
1746
- res.status(500).json({
1747
- jsonrpc: "2.0",
1748
- error: {
1749
- code: -32603,
1750
- message: errorMessage
1751
- },
1752
- id: null
1753
- });
1754
- }
1755
- }
1756
- });
1757
- // Handle GET requests for SSE streams
1758
- app.get("/mcp", async (req, res) => {
1759
- const sessionId = req.headers["mcp-session-id"];
1760
- if (!sessionId) {
1761
- res.status(400).json({
1762
- jsonrpc: "2.0",
1763
- error: { code: -32000, message: "Missing session ID for SSE stream" },
1764
- id: null
1765
- });
1766
- return;
1767
- }
1768
- const session = sessions.get(sessionId);
1769
- if (!session) {
1770
- res.status(400).json({
1771
- jsonrpc: "2.0",
1772
- error: { code: -32000, message: "Invalid or expired session" },
1773
- id: null
1774
- });
1775
- return;
1776
- }
1777
- // Update last activity timestamp for session keep-alive
1778
- session.lastActivityAt = Date.now();
1779
- try {
1780
- await session.transport.handleRequest(req, res);
1781
- }
1782
- catch (error) {
1783
- console.error("Error handling MCP GET request:", error);
1784
- if (!res.headersSent) {
1785
- res.status(500).json({
1786
- jsonrpc: "2.0",
1787
- error: { code: -32603, message: "SSE stream error" },
1788
- id: null
1789
- });
1790
- }
1791
- }
1792
- });
1793
- // Handle DELETE requests for session termination
1794
- app.delete("/mcp", async (req, res) => {
1795
- const sessionId = req.headers["mcp-session-id"];
1796
- if (sessionId && sessions.has(sessionId)) {
1797
- const session = sessions.get(sessionId);
1798
- sessions.delete(sessionId);
1799
- try {
1800
- await session?.server.close();
1801
- }
1802
- catch (e) {
1803
- if (DEBUG) {
1804
- console.error(`[DEBUG] Error closing session on DELETE:`, e);
1805
- }
1806
- }
1807
- if (DEBUG) {
1808
- console.error(`[DEBUG] Session terminated: ${sessionId}`);
1809
- }
1810
- }
1811
- res.status(200).json({ message: "Session terminated" });
1812
- });
1813
- // Start the HTTP server
1814
- app.listen(HTTP_PORT, HTTP_HOST, () => {
1815
- console.error(`Sequentum MCP Server running on HTTP`);
1816
- console.error(` URL: http://${HTTP_HOST}:${HTTP_PORT}/mcp`);
1817
- console.error(` Transport: Streamable HTTP`);
1818
- console.error(` Connected to: ${API_BASE_URL}`);
1819
- console.error(` Health check: http://${HTTP_HOST}:${HTTP_PORT}/health`);
1820
- });
1821
- }
1822
96
  async function main() {
1823
97
  if (TRANSPORT_MODE === "http") {
1824
- await startHttpServer();
98
+ await startHttpServer(API_BASE_URL, version, HTTP_PORT, HTTP_HOST);
1825
99
  }
1826
100
  else {
1827
101
  await startStdioServer();