skedyul 0.3.0 → 0.3.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 (77) hide show
  1. package/dist/.build-stamp +1 -1
  2. package/dist/config/app-config.d.ts +73 -0
  3. package/dist/config/app-config.js +12 -0
  4. package/dist/config/index.d.ts +9 -0
  5. package/dist/config/index.js +33 -0
  6. package/dist/config/loader.d.ts +7 -0
  7. package/dist/config/loader.js +119 -0
  8. package/dist/config/types/agent.d.ts +29 -0
  9. package/dist/config/types/agent.js +5 -0
  10. package/dist/config/types/channel.d.ts +46 -0
  11. package/dist/config/types/channel.js +2 -0
  12. package/dist/config/types/compute.d.ts +1 -0
  13. package/dist/config/types/compute.js +5 -0
  14. package/dist/config/types/env.d.ts +16 -0
  15. package/dist/config/types/env.js +5 -0
  16. package/dist/config/types/index.d.ts +9 -0
  17. package/dist/config/types/index.js +26 -0
  18. package/dist/config/types/model.d.ts +62 -0
  19. package/dist/config/types/model.js +2 -0
  20. package/dist/config/types/page.d.ts +436 -0
  21. package/dist/config/types/page.js +5 -0
  22. package/dist/config/types/resource.d.ts +30 -0
  23. package/dist/config/types/resource.js +5 -0
  24. package/dist/config/types/webhook.d.ts +35 -0
  25. package/dist/config/types/webhook.js +5 -0
  26. package/dist/config/types/workflow.d.ts +24 -0
  27. package/dist/config/types/workflow.js +2 -0
  28. package/dist/config/utils.d.ts +16 -0
  29. package/dist/config/utils.js +37 -0
  30. package/dist/config.d.ts +5 -767
  31. package/dist/config.js +11 -151
  32. package/dist/schemas.d.ts +43 -43
  33. package/dist/server/core-api-handler.d.ts +8 -0
  34. package/dist/server/core-api-handler.js +148 -0
  35. package/dist/server/dedicated.d.ts +7 -0
  36. package/dist/server/dedicated.js +610 -0
  37. package/dist/server/handler-helpers.d.ts +24 -0
  38. package/dist/server/handler-helpers.js +75 -0
  39. package/dist/server/index.d.ts +19 -0
  40. package/dist/server/index.js +196 -0
  41. package/dist/server/serverless.d.ts +7 -0
  42. package/dist/server/serverless.js +629 -0
  43. package/dist/server/startup-logger.d.ts +9 -0
  44. package/dist/server/startup-logger.js +113 -0
  45. package/dist/server/tool-handler.d.ts +14 -0
  46. package/dist/server/tool-handler.js +189 -0
  47. package/dist/server/types.d.ts +22 -0
  48. package/dist/server/types.js +2 -0
  49. package/dist/server/utils/env.d.ts +12 -0
  50. package/dist/server/utils/env.js +38 -0
  51. package/dist/server/utils/http.d.ts +30 -0
  52. package/dist/server/utils/http.js +81 -0
  53. package/dist/server/utils/index.d.ts +3 -0
  54. package/dist/server/utils/index.js +24 -0
  55. package/dist/server/utils/schema.d.ts +22 -0
  56. package/dist/server/utils/schema.js +102 -0
  57. package/dist/server.d.ts +7 -11
  58. package/dist/server.js +39 -2026
  59. package/dist/types/aws.d.ts +15 -0
  60. package/dist/types/aws.js +5 -0
  61. package/dist/types/handlers.d.ts +122 -0
  62. package/dist/types/handlers.js +2 -0
  63. package/dist/types/index.d.ts +16 -0
  64. package/dist/types/index.js +16 -0
  65. package/dist/types/server.d.ts +43 -0
  66. package/dist/types/server.js +2 -0
  67. package/dist/types/shared.d.ts +16 -0
  68. package/dist/types/shared.js +5 -0
  69. package/dist/types/tool-context.d.ts +64 -0
  70. package/dist/types/tool-context.js +12 -0
  71. package/dist/types/tool.d.ts +96 -0
  72. package/dist/types/tool.js +19 -0
  73. package/dist/types/webhook.d.ts +116 -0
  74. package/dist/types/webhook.js +7 -0
  75. package/dist/types.d.ts +4 -461
  76. package/dist/types.js +21 -31
  77. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -1,2029 +1,42 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.server = void 0;
40
- exports.createSkedyulServer = createSkedyulServer;
41
- const http_1 = __importDefault(require("http"));
42
- const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
43
- const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
44
- const z = __importStar(require("zod"));
45
- const service_1 = require("./core/service");
46
- const client_1 = require("./core/client");
47
- const errors_1 = require("./errors");
48
- function normalizeBilling(billing) {
49
- if (!billing || typeof billing.credits !== 'number') {
50
- return { credits: 0 };
51
- }
52
- return billing;
53
- }
54
- function toJsonSchema(schema) {
55
- if (!schema)
56
- return undefined;
57
- try {
58
- // Zod v4 has native JSON Schema support via z.toJSONSchema()
59
- return z.toJSONSchema(schema, {
60
- unrepresentable: 'any', // Handle z.date(), z.bigint() etc gracefully
61
- });
62
- }
63
- catch (err) {
64
- console.error('[toJsonSchema] Failed to convert schema:', err);
65
- return undefined;
66
- }
67
- }
68
- function isToolSchemaWithJson(schema) {
69
- return Boolean(schema &&
70
- typeof schema === 'object' &&
71
- 'zod' in schema &&
72
- schema.zod instanceof z.ZodType);
73
- }
74
- function getZodSchema(schema) {
75
- if (!schema)
76
- return undefined;
77
- if (schema instanceof z.ZodType) {
78
- return schema;
79
- }
80
- if (isToolSchemaWithJson(schema)) {
81
- return schema.zod;
82
- }
83
- return undefined;
84
- }
85
- function getJsonSchemaFromToolSchema(schema) {
86
- if (!schema)
87
- return undefined;
88
- if (isToolSchemaWithJson(schema) && schema.jsonSchema) {
89
- return schema.jsonSchema;
90
- }
91
- const zodSchema = getZodSchema(schema);
92
- return toJsonSchema(zodSchema);
93
- }
94
- function parseJsonRecord(value) {
95
- if (!value) {
96
- return {};
97
- }
98
- try {
99
- return JSON.parse(value);
100
- }
101
- catch {
102
- return {};
103
- }
104
- }
105
- function parseNumberEnv(value) {
106
- if (!value) {
107
- return null;
108
- }
109
- const parsed = Number.parseInt(value, 10);
110
- return Number.isNaN(parsed) ? null : parsed;
111
- }
112
- function mergeRuntimeEnv() {
113
- const bakedEnv = parseJsonRecord(process.env.MCP_ENV_JSON);
114
- const runtimeEnv = parseJsonRecord(process.env.MCP_ENV);
115
- const merged = { ...bakedEnv, ...runtimeEnv };
116
- Object.assign(process.env, merged);
117
- }
118
- async function handleCoreMethod(method, params) {
119
- const service = service_1.coreApiService.getService();
120
- if (!service) {
121
- return {
122
- status: 404,
123
- payload: { error: 'Core API service not configured' },
124
- };
125
- }
126
- if (method === 'createCommunicationChannel') {
127
- if (!params?.channel) {
128
- return { status: 400, payload: { error: 'channel is required' } };
129
- }
130
- const channel = params.channel;
131
- const result = await service_1.coreApiService.callCreateChannel(channel);
132
- if (!result) {
133
- return {
134
- status: 500,
135
- payload: { error: 'Core API service did not respond' },
136
- };
137
- }
138
- return { status: 200, payload: result };
139
- }
140
- if (method === 'updateCommunicationChannel') {
141
- if (!params?.channel) {
142
- return { status: 400, payload: { error: 'channel is required' } };
143
- }
144
- const channel = params.channel;
145
- const result = await service_1.coreApiService.callUpdateChannel(channel);
146
- if (!result) {
147
- return {
148
- status: 500,
149
- payload: { error: 'Core API service did not respond' },
150
- };
151
- }
152
- return { status: 200, payload: result };
153
- }
154
- if (method === 'deleteCommunicationChannel') {
155
- if (!params?.id || typeof params.id !== 'string') {
156
- return { status: 400, payload: { error: 'id is required' } };
157
- }
158
- const result = await service_1.coreApiService.callDeleteChannel(params.id);
159
- if (!result) {
160
- return {
161
- status: 500,
162
- payload: { error: 'Core API service did not respond' },
163
- };
164
- }
165
- return { status: 200, payload: result };
166
- }
167
- if (method === 'getCommunicationChannel') {
168
- if (!params?.id || typeof params.id !== 'string') {
169
- return { status: 400, payload: { error: 'id is required' } };
170
- }
171
- const result = await service_1.coreApiService.callGetChannel(params.id);
172
- if (!result) {
173
- return {
174
- status: 404,
175
- payload: { error: 'Channel not found' },
176
- };
177
- }
178
- return { status: 200, payload: result };
179
- }
180
- if (method === 'getCommunicationChannels') {
181
- const result = await service_1.coreApiService.callListChannels();
182
- if (!result) {
183
- return {
184
- status: 500,
185
- payload: { error: 'Core API service did not respond' },
186
- };
187
- }
188
- return { status: 200, payload: result };
189
- }
190
- if (method === 'communicationChannel.list') {
191
- const result = await service_1.coreApiService.callListChannels();
192
- if (!result) {
193
- return {
194
- status: 500,
195
- payload: { error: 'Core API service did not respond' },
196
- };
197
- }
198
- return { status: 200, payload: result };
199
- }
200
- if (method === 'communicationChannel.get') {
201
- if (!params?.id || typeof params.id !== 'string') {
202
- return { status: 400, payload: { error: 'id is required' } };
203
- }
204
- const result = await service_1.coreApiService.callGetChannel(params.id);
205
- if (!result) {
206
- return {
207
- status: 404,
208
- payload: { error: 'Channel not found' },
209
- };
210
- }
211
- return { status: 200, payload: result };
212
- }
213
- if (method === 'workplace.list') {
214
- const result = await service_1.coreApiService.callListWorkplaces();
215
- if (!result) {
216
- return {
217
- status: 500,
218
- payload: { error: 'Core API service did not respond' },
219
- };
220
- }
221
- return { status: 200, payload: result };
222
- }
223
- if (method === 'workplace.get') {
224
- if (!params?.id || typeof params.id !== 'string') {
225
- return { status: 400, payload: { error: 'id is required' } };
226
- }
227
- const result = await service_1.coreApiService.callGetWorkplace(params.id);
228
- if (!result) {
229
- return {
230
- status: 404,
231
- payload: { error: 'Workplace not found' },
232
- };
233
- }
234
- return { status: 200, payload: result };
235
- }
236
- if (method === 'sendMessage') {
237
- if (!params?.message || !params?.communicationChannel) {
238
- return { status: 400, payload: { error: 'message and communicationChannel are required' } };
239
- }
240
- const msg = params.message;
241
- const channel = params.communicationChannel;
242
- const result = await service_1.coreApiService.callSendMessage({
243
- message: msg,
244
- communicationChannel: channel,
245
- });
246
- if (!result) {
247
- return {
248
- status: 500,
249
- payload: { error: 'Core API service did not respond' },
250
- };
251
- }
252
- return { status: 200, payload: result };
253
- }
254
- return {
255
- status: 400,
256
- payload: { error: 'Unknown core method' },
257
- };
258
- }
259
- function buildToolMetadata(registry) {
260
- return Object.values(registry).map((tool) => {
261
- const timeout = typeof tool.timeout === 'number' && tool.timeout > 0 ? tool.timeout : 10000;
262
- return {
263
- name: tool.name,
264
- displayName: tool.label || tool.name,
265
- description: tool.description,
266
- inputSchema: getJsonSchemaFromToolSchema(tool.inputSchema),
267
- outputSchema: getJsonSchemaFromToolSchema(tool.outputSchema),
268
- timeout, // Default to 10 seconds
269
- };
270
- });
271
- }
272
- function createRequestState(maxRequests, ttlExtendSeconds, runtimeLabel, toolNames) {
273
- let requestCount = 0;
274
- let lastRequestTime = Date.now();
275
- return {
276
- incrementRequestCount() {
277
- requestCount += 1;
278
- lastRequestTime = Date.now();
279
- },
280
- shouldShutdown() {
281
- return maxRequests !== null && requestCount >= maxRequests;
282
- },
283
- getHealthStatus() {
284
- return {
285
- status: 'running',
286
- requests: requestCount,
287
- maxRequests,
288
- requestsRemaining: maxRequests !== null ? Math.max(0, maxRequests - requestCount) : null,
289
- lastRequestTime,
290
- ttlExtendSeconds,
291
- runtime: runtimeLabel,
292
- tools: [...toolNames],
293
- };
294
- },
295
- };
296
- }
297
- function createCallToolHandler(registry, state, onMaxRequests) {
298
- return async function callTool(nameRaw, argsRaw) {
299
- const toolName = String(nameRaw);
300
- const tool = registry[toolName];
301
- if (!tool) {
302
- throw new Error(`Tool "${toolName}" not found in registry`);
303
- }
304
- if (!tool.handler || typeof tool.handler !== 'function') {
305
- throw new Error(`Tool "${toolName}" handler is not a function`);
306
- }
307
- const fn = tool.handler;
308
- const args = (argsRaw ?? {});
309
- const estimateMode = args.estimate === true;
310
- if (!estimateMode) {
311
- state.incrementRequestCount();
312
- if (state.shouldShutdown()) {
313
- onMaxRequests?.();
314
- }
315
- }
316
- const requestEnv = args.env ?? {};
317
- const originalEnv = { ...process.env };
318
- Object.assign(process.env, requestEnv);
319
- try {
320
- // Get tool inputs (clean, no context)
321
- const inputs = (args.inputs ?? {});
322
- // Get context from args.context (separate from inputs)
323
- const rawContext = (args.context ?? {});
324
- // Debug logging for tool handler
325
- console.log('\n🔧 callTool processing:');
326
- console.log(' Full args received:', JSON.stringify(args, null, 2));
327
- console.log(' args.context:', JSON.stringify(args.context, null, 2));
328
- console.log(' rawContext:', JSON.stringify(rawContext, null, 2));
329
- // Extract app info (required for all contexts)
330
- const app = rawContext.app;
331
- // Determine trigger type from context
332
- const trigger = rawContext.trigger || 'agent';
333
- // Build execution context based on trigger type
334
- let executionContext;
335
- if (trigger === 'provision') {
336
- // Provision context - no installation, no workplace
337
- executionContext = {
338
- trigger: 'provision',
339
- app,
340
- env: process.env,
341
- mode: estimateMode ? 'estimate' : 'execute',
342
- };
343
- }
344
- else {
345
- // Runtime context - has installation, workplace, request
346
- const workplace = rawContext.workplace;
347
- const request = rawContext.request;
348
- const appInstallationId = rawContext.appInstallationId;
349
- const envVars = process.env;
350
- const modeValue = estimateMode ? 'estimate' : 'execute';
351
- if (trigger === 'field_change') {
352
- const field = rawContext.field;
353
- executionContext = { trigger: 'field_change', app, appInstallationId, workplace, request, env: envVars, mode: modeValue, field };
354
- }
355
- else if (trigger === 'page_action') {
356
- const page = rawContext.page;
357
- executionContext = { trigger: 'page_action', app, appInstallationId, workplace, request, env: envVars, mode: modeValue, page };
358
- }
359
- else if (trigger === 'form_submit') {
360
- const form = rawContext.form;
361
- executionContext = { trigger: 'form_submit', app, appInstallationId, workplace, request, env: envVars, mode: modeValue, form };
362
- }
363
- else if (trigger === 'workflow') {
364
- executionContext = { trigger: 'workflow', app, appInstallationId, workplace, request, env: envVars, mode: modeValue };
365
- }
366
- else if (trigger === 'page_context') {
367
- // Page context trigger - similar to agent but for page context resolution
368
- executionContext = { trigger: 'agent', app, appInstallationId, workplace, request, env: envVars, mode: modeValue };
369
- }
370
- else {
371
- // Default to agent
372
- executionContext = { trigger: 'agent', app, appInstallationId, workplace, request, env: envVars, mode: modeValue };
373
- }
374
- }
375
- console.log(' Built executionContext:', JSON.stringify({
376
- trigger: executionContext.trigger,
377
- app: executionContext.app,
378
- appInstallationId: 'appInstallationId' in executionContext ? executionContext.appInstallationId : undefined,
379
- workplace: 'workplace' in executionContext ? executionContext.workplace : undefined,
380
- request: 'request' in executionContext ? executionContext.request : undefined,
381
- mode: executionContext.mode,
382
- }, null, 2));
383
- // Build request-scoped config from env passed in MCP call
384
- const requestConfig = {
385
- baseUrl: requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? '',
386
- apiToken: requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? '',
387
- };
388
- console.log(' Request config:', JSON.stringify({
389
- baseUrl: requestConfig.baseUrl ? '(set)' : '(empty)',
390
- apiToken: requestConfig.apiToken ? '(set)' : '(empty)',
391
- }, null, 2));
392
- // Call handler with two arguments: (input, context)
393
- // Wrap in runWithConfig for request-scoped SDK configuration
394
- const functionResult = await (0, client_1.runWithConfig)(requestConfig, async () => {
395
- return await fn(inputs, executionContext);
396
- });
397
- const billing = normalizeBilling(functionResult.billing);
398
- return {
399
- output: functionResult.output,
400
- billing,
401
- meta: functionResult.meta ?? {
402
- success: true,
403
- message: 'OK',
404
- toolName,
405
- },
406
- effect: functionResult.effect,
407
- };
408
- }
409
- catch (error) {
410
- // Check if it's an AppAuthInvalidError
411
- if (error instanceof errors_1.AppAuthInvalidError) {
412
- return {
413
- output: null,
414
- billing: { credits: 0 },
415
- meta: {
416
- success: false,
417
- message: error.message,
418
- toolName,
419
- },
420
- error: {
421
- code: error.code,
422
- message: error.message,
423
- },
424
- // Note: redirect URL will be added by workflow after detecting APP_AUTH_INVALID
425
- };
426
- }
427
- // Generic error handling for other errors
428
- const errorMessage = error instanceof Error ? error.message : String(error ?? '');
429
- return {
430
- output: null,
431
- billing: { credits: 0 },
432
- meta: {
433
- success: false,
434
- message: errorMessage,
435
- toolName,
436
- },
437
- error: {
438
- code: 'TOOL_EXECUTION_ERROR',
439
- message: errorMessage,
440
- },
441
- };
442
- }
443
- finally {
444
- process.env = originalEnv;
445
- }
446
- };
447
- }
448
- function readRawRequestBody(req) {
449
- return new Promise((resolve, reject) => {
450
- let body = '';
451
- req.on('data', (chunk) => {
452
- body += chunk.toString();
453
- });
454
- req.on('end', () => {
455
- resolve(body);
456
- });
457
- req.on('error', reject);
458
- });
459
- }
460
- async function parseJSONBody(req) {
461
- const rawBody = await readRawRequestBody(req);
462
- try {
463
- return rawBody ? JSON.parse(rawBody) : {};
464
- }
465
- catch (err) {
466
- throw err;
467
- }
468
- }
469
- function sendJSON(res, statusCode, data) {
470
- res.writeHead(statusCode, { 'Content-Type': 'application/json' });
471
- res.end(JSON.stringify(data));
472
- }
473
- function sendHTML(res, statusCode, html) {
474
- res.writeHead(statusCode, { 'Content-Type': 'text/html; charset=utf-8' });
475
- res.end(html);
476
- }
477
- function getDefaultHeaders(options) {
478
- return {
479
- 'Content-Type': 'application/json',
480
- 'Access-Control-Allow-Origin': options?.allowOrigin ?? '*',
481
- 'Access-Control-Allow-Methods': options?.allowMethods ?? 'GET, POST, OPTIONS',
482
- 'Access-Control-Allow-Headers': options?.allowHeaders ?? 'Content-Type',
483
- };
484
- }
485
- function createResponse(statusCode, body, headers) {
486
- return {
487
- statusCode,
488
- headers,
489
- body: JSON.stringify(body),
490
- };
491
- }
492
- function getListeningPort(config) {
493
- const envPort = Number.parseInt(process.env.PORT ?? '', 10);
494
- if (!Number.isNaN(envPort)) {
495
- return envPort;
496
- }
497
- return config.defaultPort ?? 3000;
498
- }
499
- /**
500
- * Prints a styled startup log showing server configuration
501
- */
502
- function printStartupLog(config, tools, webhookRegistry, port) {
503
- const webhookCount = webhookRegistry ? Object.keys(webhookRegistry).length : 0;
504
- const webhookNames = webhookRegistry ? Object.keys(webhookRegistry) : [];
505
- const maxRequests = config.maxRequests ??
506
- parseNumberEnv(process.env.MCP_MAX_REQUESTS) ??
507
- null;
508
- const ttlExtendSeconds = config.ttlExtendSeconds ??
509
- parseNumberEnv(process.env.MCP_TTL_EXTEND) ??
510
- 3600;
511
- const executableId = process.env.SKEDYUL_EXECUTABLE_ID || 'local';
512
- const divider = '═'.repeat(70);
513
- const thinDivider = '─'.repeat(70);
514
- // eslint-disable-next-line no-console
515
- console.log('');
516
- // eslint-disable-next-line no-console
517
- console.log(`╔${divider}╗`);
518
- // eslint-disable-next-line no-console
519
- console.log(`║ 🚀 Skedyul MCP Server Starting ║`);
520
- // eslint-disable-next-line no-console
521
- console.log(`╠${divider}╣`);
522
- // eslint-disable-next-line no-console
523
- console.log(`║ ║`);
524
- // eslint-disable-next-line no-console
525
- console.log(`║ 📦 Server: ${padEnd(config.metadata.name, 49)}║`);
526
- // eslint-disable-next-line no-console
527
- console.log(`║ 🏷️ Version: ${padEnd(config.metadata.version, 49)}║`);
528
- // eslint-disable-next-line no-console
529
- console.log(`║ ⚡ Compute: ${padEnd(config.computeLayer, 49)}║`);
530
- if (port) {
531
- // eslint-disable-next-line no-console
532
- console.log(`║ 🌐 Port: ${padEnd(String(port), 49)}║`);
533
- }
534
- // eslint-disable-next-line no-console
535
- console.log(`║ 🔑 Executable: ${padEnd(executableId, 49)}║`);
536
- // eslint-disable-next-line no-console
537
- console.log(`║ ║`);
538
- // eslint-disable-next-line no-console
539
- console.log(`╟${thinDivider}╢`);
540
- // eslint-disable-next-line no-console
541
- console.log(`║ ║`);
542
- // eslint-disable-next-line no-console
543
- console.log(`║ 🔧 Tools (${tools.length}): ║`);
544
- // List tools (max 10, then show "and X more...")
545
- const maxToolsToShow = 10;
546
- const toolsToShow = tools.slice(0, maxToolsToShow);
547
- for (const tool of toolsToShow) {
548
- // eslint-disable-next-line no-console
549
- console.log(`║ • ${padEnd(tool.name, 61)}║`);
550
- }
551
- if (tools.length > maxToolsToShow) {
552
- // eslint-disable-next-line no-console
553
- console.log(`║ ... and ${tools.length - maxToolsToShow} more ║`);
554
- }
555
- if (webhookCount > 0) {
556
- // eslint-disable-next-line no-console
557
- console.log(`║ ║`);
558
- // eslint-disable-next-line no-console
559
- console.log(`║ 🪝 Webhooks (${webhookCount}): ║`);
560
- const maxWebhooksToShow = 5;
561
- const webhooksToShow = webhookNames.slice(0, maxWebhooksToShow);
562
- for (const name of webhooksToShow) {
563
- // eslint-disable-next-line no-console
564
- console.log(`║ • /webhooks/${padEnd(name, 51)}║`);
565
- }
566
- if (webhookCount > maxWebhooksToShow) {
567
- // eslint-disable-next-line no-console
568
- console.log(`║ ... and ${webhookCount - maxWebhooksToShow} more ║`);
569
- }
570
- }
571
- // eslint-disable-next-line no-console
572
- console.log(`║ ║`);
573
- // eslint-disable-next-line no-console
574
- console.log(`╟${thinDivider}╢`);
575
- // eslint-disable-next-line no-console
576
- console.log(`║ ║`);
577
- // eslint-disable-next-line no-console
578
- console.log(`║ ⚙️ Configuration: ║`);
579
- // eslint-disable-next-line no-console
580
- console.log(`║ Max Requests: ${padEnd(maxRequests !== null ? String(maxRequests) : 'unlimited', 46)}║`);
581
- // eslint-disable-next-line no-console
582
- console.log(`║ TTL Extend: ${padEnd(`${ttlExtendSeconds}s`, 46)}║`);
583
- // eslint-disable-next-line no-console
584
- console.log(`║ ║`);
585
- // eslint-disable-next-line no-console
586
- console.log(`╟${thinDivider}╢`);
587
- // eslint-disable-next-line no-console
588
- console.log(`║ ✅ Ready at ${padEnd(new Date().toISOString(), 55)}║`);
589
- // eslint-disable-next-line no-console
590
- console.log(`╚${divider}╝`);
591
- // eslint-disable-next-line no-console
592
- console.log('');
593
- }
594
- /**
595
- * Pad a string to the right with spaces
596
- */
597
- function padEnd(str, length) {
598
- if (str.length >= length) {
599
- return str.slice(0, length);
600
- }
601
- return str + ' '.repeat(length - str.length);
602
- }
603
- function createSkedyulServer(config, registry, webhookRegistry) {
604
- mergeRuntimeEnv();
605
- if (config.coreApi?.service) {
606
- service_1.coreApiService.register(config.coreApi.service);
607
- if (config.coreApi.webhookHandler) {
608
- service_1.coreApiService.setWebhookHandler(config.coreApi.webhookHandler);
609
- }
610
- }
611
- const tools = buildToolMetadata(registry);
612
- const toolNames = Object.values(registry).map((tool) => tool.name);
613
- const runtimeLabel = config.computeLayer;
614
- const maxRequests = config.maxRequests ??
615
- parseNumberEnv(process.env.MCP_MAX_REQUESTS) ??
616
- null;
617
- const ttlExtendSeconds = config.ttlExtendSeconds ??
618
- parseNumberEnv(process.env.MCP_TTL_EXTEND) ??
619
- 3600;
620
- const state = createRequestState(maxRequests, ttlExtendSeconds, runtimeLabel, toolNames);
621
- const mcpServer = new mcp_js_1.McpServer({
622
- name: config.metadata.name,
623
- version: config.metadata.version,
624
- });
625
- const dedicatedShutdown = () => {
626
- // eslint-disable-next-line no-console
627
- console.log('Max requests reached, shutting down...');
628
- setTimeout(() => process.exit(0), 1000);
629
- };
630
- const callTool = createCallToolHandler(registry, state, config.computeLayer === 'dedicated' ? dedicatedShutdown : undefined);
631
- // Register all tools from the registry
632
- for (const [toolKey, tool] of Object.entries(registry)) {
633
- // Use the tool's name or fall back to the registry key
634
- const toolName = tool.name || toolKey;
635
- const toolDisplayName = tool.label || toolName;
636
- const inputZodSchema = getZodSchema(tool.inputSchema);
637
- const outputZodSchema = getZodSchema(tool.outputSchema);
638
- // Wrap the input schema to accept Skedyul format: { inputs: {...}, env: {...} }
639
- // This allows the MCP SDK to pass through the wrapper without stripping fields
640
- const wrappedInputSchema = z.object({
641
- inputs: inputZodSchema ?? z.record(z.string(), z.unknown()).optional(),
642
- env: z.record(z.string(), z.string()).optional(),
643
- }).passthrough();
644
- mcpServer.registerTool(toolName, {
645
- title: toolDisplayName,
646
- description: tool.description,
647
- inputSchema: wrappedInputSchema,
648
- outputSchema: outputZodSchema,
649
- }, async (args) => {
650
- // Args are in Skedyul format: { inputs: {...}, context: {...}, env: {...} }
651
- const rawArgs = args;
652
- const toolInputs = (rawArgs.inputs ?? {});
653
- const toolContext = rawArgs.context;
654
- const toolEnv = rawArgs.env;
655
- // Debug logging for MCP SDK tool calls
656
- console.log('\n📞 MCP SDK registerTool handler:');
657
- console.log(' Tool:', toolName);
658
- console.log(' Raw args:', JSON.stringify(rawArgs, null, 2));
659
- console.log(' Extracted context:', JSON.stringify(toolContext, null, 2));
660
- // Validate inputs if schema exists
661
- let validatedInputs = toolInputs;
662
- if (inputZodSchema) {
663
- try {
664
- validatedInputs = inputZodSchema.parse(toolInputs);
665
- }
666
- catch (error) {
667
- console.error(`[registerTool] Input validation failed for tool ${toolName}:`, error);
668
- // Return error response instead of throwing
669
- return {
670
- content: [
671
- {
672
- type: 'text',
673
- text: JSON.stringify({
674
- error: `Input validation failed: ${error instanceof Error ? error.message : String(error)}`,
675
- }),
676
- },
677
- ],
678
- structuredContent: {
679
- error: `Input validation failed: ${error instanceof Error ? error.message : String(error)}`,
680
- },
681
- isError: true,
682
- billing: { credits: 0 },
683
- };
684
- }
685
- }
686
- const result = await callTool(toolKey, {
687
- inputs: validatedInputs,
688
- context: toolContext,
689
- env: toolEnv,
690
- });
691
- // Handle error case
692
- if (result.error) {
693
- const errorOutput = { error: result.error };
694
- return {
695
- content: [{ type: 'text', text: JSON.stringify(errorOutput) }],
696
- structuredContent: errorOutput,
697
- isError: true,
698
- billing: result.billing,
699
- };
700
- }
701
- // Transform internal format to MCP protocol format
702
- // Note: effect is embedded in structuredContent because the MCP SDK
703
- // transport strips custom top-level fields in dedicated mode
704
- const outputData = result.output;
705
- const structuredContent = outputData
706
- ? { ...outputData, __effect: result.effect }
707
- : result.effect
708
- ? { __effect: result.effect }
709
- : undefined;
710
- return {
711
- content: [{ type: 'text', text: JSON.stringify(result.output) }],
712
- structuredContent,
713
- billing: result.billing,
714
- };
715
- });
716
- }
717
- if (config.computeLayer === 'dedicated') {
718
- return createDedicatedServerInstance(config, tools, callTool, state, mcpServer, webhookRegistry);
719
- }
720
- return createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry);
721
- }
722
- // ─────────────────────────────────────────────────────────────────────────────
723
- // Shared Handler Helpers (used by both webhooks and OAuth callbacks)
724
- // ─────────────────────────────────────────────────────────────────────────────
725
2
  /**
726
- * Parses a handler envelope from the request body.
727
- * Detects envelope format: { env: {...}, request: {...}, context?: {...} }
728
- * Returns the extracted env and request, or null if not an envelope.
3
+ * Server module - re-exports from the server folder
4
+ *
5
+ * This file maintains backward compatibility while the actual implementation
6
+ * has been split into smaller, focused modules in the server/ folder.
729
7
  */
730
- function parseHandlerEnvelope(parsedBody) {
731
- // Check if parsedBody is an object with env and request properties
732
- if (typeof parsedBody !== 'object' ||
733
- parsedBody === null ||
734
- Array.isArray(parsedBody) ||
735
- !('env' in parsedBody) ||
736
- !('request' in parsedBody)) {
737
- return null;
738
- }
739
- const envelope = parsedBody;
740
- // Validate env is an object (not null, not array)
741
- if (typeof envelope.env !== 'object' ||
742
- envelope.env === null ||
743
- Array.isArray(envelope.env)) {
744
- return null;
745
- }
746
- // Validate request is an object (structure validation happens in buildRequestFromRaw)
747
- if (typeof envelope.request !== 'object' ||
748
- envelope.request === null ||
749
- Array.isArray(envelope.request)) {
750
- return null;
751
- }
752
- return {
753
- env: envelope.env,
754
- request: envelope.request,
755
- context: envelope.context,
756
- };
757
- }
758
- /**
759
- * Converts a raw HandlerRawRequest (wire format) to a rich WebhookRequest.
760
- * Parses JSON body if content-type is application/json, creates Buffer rawBody.
761
- */
762
- function buildRequestFromRaw(raw) {
763
- // Parse the original request body
764
- let parsedBody = raw.body;
765
- const contentType = raw.headers['content-type'] ?? '';
766
- if (contentType.includes('application/json')) {
767
- try {
768
- parsedBody = raw.body ? JSON.parse(raw.body) : {};
769
- }
770
- catch {
771
- // Keep as string if JSON parsing fails
772
- parsedBody = raw.body;
773
- }
774
- }
775
- return {
776
- method: raw.method,
777
- url: raw.url,
778
- path: raw.path,
779
- headers: raw.headers,
780
- query: raw.query,
781
- body: parsedBody,
782
- rawBody: raw.body ? Buffer.from(raw.body, 'utf-8') : undefined,
783
- };
784
- }
785
- /**
786
- * Builds request-scoped config by merging env from envelope with process.env fallbacks.
787
- * Used for SKEDYUL_API_TOKEN and SKEDYUL_API_URL overrides.
788
- */
789
- function buildRequestScopedConfig(env) {
790
- return {
791
- baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? '',
792
- apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? '',
793
- };
794
- }
795
- function createDedicatedServerInstance(config, tools, callTool, state, mcpServer, webhookRegistry) {
796
- const port = getListeningPort(config);
797
- const httpServer = http_1.default.createServer(async (req, res) => {
798
- function sendCoreResult(result) {
799
- sendJSON(res, result.status, result.payload);
800
- }
801
- try {
802
- const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
803
- const pathname = url.pathname;
804
- if (pathname === '/health' && req.method === 'GET') {
805
- sendJSON(res, 200, state.getHealthStatus());
806
- return;
807
- }
808
- // Handle webhook requests: /webhooks/{handle}
809
- if (pathname.startsWith('/webhooks/') && webhookRegistry) {
810
- const handle = pathname.slice('/webhooks/'.length);
811
- const webhookDef = webhookRegistry[handle];
812
- if (!webhookDef) {
813
- sendJSON(res, 404, { error: `Webhook handler '${handle}' not found` });
814
- return;
815
- }
816
- // Check if HTTP method is allowed
817
- const allowedMethods = webhookDef.methods ?? ['POST'];
818
- if (!allowedMethods.includes(req.method)) {
819
- sendJSON(res, 405, { error: `Method ${req.method} not allowed` });
820
- return;
821
- }
822
- // Read raw request body
823
- let rawBody;
824
- try {
825
- rawBody = await readRawRequestBody(req);
826
- }
827
- catch {
828
- sendJSON(res, 400, { error: 'Failed to read request body' });
829
- return;
830
- }
831
- // Parse body based on content type
832
- let parsedBody;
833
- const contentType = req.headers['content-type'] ?? '';
834
- if (contentType.includes('application/json')) {
835
- try {
836
- parsedBody = rawBody ? JSON.parse(rawBody) : {};
837
- }
838
- catch {
839
- parsedBody = rawBody;
840
- }
841
- }
842
- else {
843
- parsedBody = rawBody;
844
- }
845
- // Check if this is an envelope format from the platform
846
- // Envelope format: { env: {...}, request: {...}, context: {...} }
847
- const envelope = parseHandlerEnvelope(parsedBody);
848
- let webhookRequest;
849
- let webhookContext;
850
- let requestEnv = {};
851
- if (envelope && 'context' in envelope && envelope.context) {
852
- // Platform envelope format - use shared helpers
853
- const context = envelope.context;
854
- requestEnv = envelope.env;
855
- // Convert raw request to rich request using shared helper
856
- webhookRequest = buildRequestFromRaw(envelope.request);
857
- const envVars = { ...process.env, ...envelope.env };
858
- const app = context.app;
859
- // Build webhook context based on whether we have installation context
860
- if (context.appInstallationId && context.workplace) {
861
- // Runtime webhook context
862
- webhookContext = {
863
- env: envVars,
864
- app,
865
- appInstallationId: context.appInstallationId,
866
- workplace: context.workplace,
867
- registration: context.registration ?? {},
868
- };
869
- }
870
- else {
871
- // Provision webhook context
872
- webhookContext = {
873
- env: envVars,
874
- app,
875
- };
876
- }
877
- }
878
- else {
879
- // Direct request format (legacy or direct calls) - requires app info from headers or fail
880
- const appId = req.headers['x-skedyul-app-id'];
881
- const appVersionId = req.headers['x-skedyul-app-version-id'];
882
- if (!appId || !appVersionId) {
883
- throw new Error('Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)');
884
- }
885
- webhookRequest = {
886
- method: req.method ?? 'POST',
887
- url: url.toString(),
888
- path: pathname,
889
- headers: req.headers,
890
- query: Object.fromEntries(url.searchParams.entries()),
891
- body: parsedBody,
892
- rawBody: rawBody ? Buffer.from(rawBody, 'utf-8') : undefined,
893
- };
894
- // Direct calls are provision-level (no installation context)
895
- webhookContext = {
896
- env: process.env,
897
- app: { id: appId, versionId: appVersionId },
898
- };
899
- }
900
- // Temporarily inject env into process.env for skedyul client to use
901
- // (same pattern as tool handler)
902
- const originalEnv = { ...process.env };
903
- Object.assign(process.env, requestEnv);
904
- // Build request-scoped config for the skedyul client
905
- // This uses AsyncLocalStorage to override the global config (same pattern as tools)
906
- const requestConfig = buildRequestScopedConfig(requestEnv);
907
- // Invoke the handler with request-scoped config
908
- let webhookResponse;
909
- try {
910
- webhookResponse = await (0, client_1.runWithConfig)(requestConfig, async () => {
911
- return await webhookDef.handler(webhookRequest, webhookContext);
912
- });
913
- }
914
- catch (err) {
915
- console.error(`Webhook handler '${handle}' error:`, err);
916
- sendJSON(res, 500, { error: 'Webhook handler error' });
917
- return;
918
- }
919
- finally {
920
- // Restore original env
921
- process.env = originalEnv;
922
- }
923
- // Send response
924
- const status = webhookResponse.status ?? 200;
925
- const responseHeaders = {
926
- ...webhookResponse.headers,
927
- };
928
- // Default to JSON content type if not specified
929
- if (!responseHeaders['Content-Type'] && !responseHeaders['content-type']) {
930
- responseHeaders['Content-Type'] = 'application/json';
931
- }
932
- res.writeHead(status, responseHeaders);
933
- if (webhookResponse.body !== undefined) {
934
- if (typeof webhookResponse.body === 'string') {
935
- res.end(webhookResponse.body);
936
- }
937
- else {
938
- res.end(JSON.stringify(webhookResponse.body));
939
- }
940
- }
941
- else {
942
- res.end();
943
- }
944
- return;
945
- }
946
- if (pathname === '/estimate' && req.method === 'POST') {
947
- let estimateBody;
948
- try {
949
- estimateBody = (await parseJSONBody(req));
950
- }
951
- catch {
952
- sendJSON(res, 400, {
953
- error: {
954
- code: -32700,
955
- message: 'Parse error',
956
- },
957
- });
958
- return;
959
- }
960
- try {
961
- const estimateResponse = await callTool(estimateBody.name, {
962
- inputs: estimateBody.inputs,
963
- estimate: true,
964
- });
965
- sendJSON(res, 200, {
966
- billing: estimateResponse.billing ?? { credits: 0 },
967
- });
968
- }
969
- catch (err) {
970
- sendJSON(res, 500, {
971
- error: {
972
- code: -32603,
973
- message: err instanceof Error ? err.message : String(err ?? ''),
974
- },
975
- });
976
- }
977
- return;
978
- }
979
- // Handle /oauth_callback endpoint for OAuth callbacks (called by Temporal workflow)
980
- if (pathname === '/oauth_callback' && req.method === 'POST') {
981
- if (!config.hooks?.oauth_callback) {
982
- sendJSON(res, 404, { error: 'OAuth callback handler not configured' });
983
- return;
984
- }
985
- let parsedBody;
986
- try {
987
- parsedBody = await parseJSONBody(req);
988
- }
989
- catch (err) {
990
- console.error('[OAuth Callback] Failed to parse JSON body:', err);
991
- sendJSON(res, 400, {
992
- error: { code: -32700, message: 'Parse error' },
993
- });
994
- return;
995
- }
996
- // Debug: Log what we received
997
- console.log('[OAuth Callback] Parsed body type:', typeof parsedBody);
998
- console.log('[OAuth Callback] Parsed body is array:', Array.isArray(parsedBody));
999
- console.log('[OAuth Callback] Parsed body has env:', parsedBody && typeof parsedBody === 'object' && 'env' in parsedBody);
1000
- console.log('[OAuth Callback] Parsed body has request:', parsedBody && typeof parsedBody === 'object' && 'request' in parsedBody);
1001
- if (parsedBody && typeof parsedBody === 'object' && !Array.isArray(parsedBody)) {
1002
- console.log('[OAuth Callback] Parsed body keys:', Object.keys(parsedBody));
1003
- }
1004
- // Parse envelope using shared helper
1005
- const envelope = parseHandlerEnvelope(parsedBody);
1006
- if (!envelope) {
1007
- console.error('[OAuth Callback] Failed to parse envelope. Body:', JSON.stringify(parsedBody, null, 2));
1008
- sendJSON(res, 400, {
1009
- error: { code: -32602, message: 'Missing envelope format: expected { env, request }' },
1010
- });
1011
- return;
1012
- }
1013
- // Convert raw request to rich request using shared helper
1014
- const oauthRequest = buildRequestFromRaw(envelope.request);
1015
- // Build request-scoped config using shared helper
1016
- const oauthCallbackRequestConfig = buildRequestScopedConfig(envelope.env);
1017
- const oauthCallbackContext = {
1018
- request: oauthRequest,
1019
- };
1020
- try {
1021
- const oauthCallbackHook = config.hooks.oauth_callback;
1022
- const oauthCallbackHandler = typeof oauthCallbackHook === 'function'
1023
- ? oauthCallbackHook
1024
- : oauthCallbackHook.handler;
1025
- const result = await (0, client_1.runWithConfig)(oauthCallbackRequestConfig, async () => {
1026
- return await oauthCallbackHandler(oauthCallbackContext);
1027
- });
1028
- sendJSON(res, 200, {
1029
- appInstallationId: result.appInstallationId,
1030
- env: result.env ?? {},
1031
- });
1032
- }
1033
- catch (err) {
1034
- const errorMessage = err instanceof Error ? err.message : String(err ?? 'Unknown error');
1035
- sendJSON(res, 500, {
1036
- error: {
1037
- code: -32603,
1038
- message: errorMessage,
1039
- },
1040
- });
1041
- }
1042
- return;
1043
- }
1044
- // Handle /install endpoint for install handlers
1045
- if (pathname === '/install' && req.method === 'POST') {
1046
- if (!config.hooks?.install) {
1047
- sendJSON(res, 404, { error: 'Install handler not configured' });
1048
- return;
1049
- }
1050
- let installBody;
1051
- try {
1052
- installBody = (await parseJSONBody(req));
1053
- }
1054
- catch {
1055
- sendJSON(res, 400, {
1056
- error: { code: -32700, message: 'Parse error' },
1057
- });
1058
- return;
1059
- }
1060
- if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
1061
- sendJSON(res, 400, {
1062
- error: { code: -32602, message: 'Missing context (appInstallationId and workplace required)' },
1063
- });
1064
- return;
1065
- }
1066
- const installContext = {
1067
- env: installBody.env ?? {},
1068
- workplace: installBody.context.workplace,
1069
- appInstallationId: installBody.context.appInstallationId,
1070
- app: installBody.context.app,
1071
- };
1072
- // Build request-scoped config for SDK access
1073
- // Use env from request body (contains generated token from workflow)
1074
- const installRequestConfig = {
1075
- baseUrl: installBody.env?.SKEDYUL_API_URL ??
1076
- process.env.SKEDYUL_API_URL ??
1077
- '',
1078
- apiToken: installBody.env?.SKEDYUL_API_TOKEN ??
1079
- process.env.SKEDYUL_API_TOKEN ??
1080
- '',
1081
- };
1082
- try {
1083
- const installHook = config.hooks.install;
1084
- const installHandler = typeof installHook === 'function'
1085
- ? installHook
1086
- : installHook.handler;
1087
- const result = await (0, client_1.runWithConfig)(installRequestConfig, async () => {
1088
- return await installHandler(installContext);
1089
- });
1090
- sendJSON(res, 200, {
1091
- env: result.env ?? {},
1092
- redirect: result.redirect,
1093
- });
1094
- }
1095
- catch (err) {
1096
- // Check for typed install errors
1097
- if (err instanceof errors_1.InstallError) {
1098
- sendJSON(res, 400, {
1099
- error: {
1100
- code: err.code,
1101
- message: err.message,
1102
- field: err.field,
1103
- },
1104
- });
1105
- }
1106
- else {
1107
- sendJSON(res, 500, {
1108
- error: {
1109
- code: -32603,
1110
- message: err instanceof Error ? err.message : String(err ?? ''),
1111
- },
1112
- });
1113
- }
1114
- }
1115
- return;
1116
- }
1117
- // Handle /uninstall endpoint for uninstall handlers
1118
- if (pathname === '/uninstall' && req.method === 'POST') {
1119
- if (!config.hooks?.uninstall) {
1120
- sendJSON(res, 404, { error: 'Uninstall handler not configured' });
1121
- return;
1122
- }
1123
- let uninstallBody;
1124
- try {
1125
- uninstallBody = (await parseJSONBody(req));
1126
- }
1127
- catch {
1128
- sendJSON(res, 400, {
1129
- error: { code: -32700, message: 'Parse error' },
1130
- });
1131
- return;
1132
- }
1133
- if (!uninstallBody.context?.appInstallationId ||
1134
- !uninstallBody.context?.workplace ||
1135
- !uninstallBody.context?.app) {
1136
- sendJSON(res, 400, {
1137
- error: {
1138
- code: -32602,
1139
- message: 'Missing context (appInstallationId, workplace and app required)',
1140
- },
1141
- });
1142
- return;
1143
- }
1144
- const uninstallContext = {
1145
- env: uninstallBody.env ?? {},
1146
- workplace: uninstallBody.context.workplace,
1147
- appInstallationId: uninstallBody.context.appInstallationId,
1148
- app: uninstallBody.context.app,
1149
- };
1150
- const uninstallRequestConfig = {
1151
- baseUrl: uninstallBody.env?.SKEDYUL_API_URL ??
1152
- process.env.SKEDYUL_API_URL ??
1153
- '',
1154
- apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ??
1155
- process.env.SKEDYUL_API_TOKEN ??
1156
- '',
1157
- };
1158
- try {
1159
- const uninstallHook = config.hooks.uninstall;
1160
- const uninstallHandlerFn = typeof uninstallHook === 'function' ? uninstallHook : uninstallHook.handler;
1161
- const result = await (0, client_1.runWithConfig)(uninstallRequestConfig, async () => {
1162
- return await uninstallHandlerFn(uninstallContext);
1163
- });
1164
- sendJSON(res, 200, {
1165
- cleanedWebhookIds: result.cleanedWebhookIds ?? [],
1166
- });
1167
- }
1168
- catch (err) {
1169
- sendJSON(res, 500, {
1170
- error: {
1171
- code: -32603,
1172
- message: err instanceof Error ? err.message : String(err ?? ''),
1173
- },
1174
- });
1175
- }
1176
- return;
1177
- }
1178
- // Handle /provision endpoint for provision handlers
1179
- if (pathname === '/provision' && req.method === 'POST') {
1180
- if (!config.hooks?.provision) {
1181
- sendJSON(res, 404, { error: 'Provision handler not configured' });
1182
- return;
1183
- }
1184
- let provisionBody;
1185
- try {
1186
- provisionBody = (await parseJSONBody(req));
1187
- }
1188
- catch {
1189
- sendJSON(res, 400, {
1190
- error: { code: -32700, message: 'Parse error' },
1191
- });
1192
- return;
1193
- }
1194
- if (!provisionBody.context?.app) {
1195
- sendJSON(res, 400, {
1196
- error: { code: -32602, message: 'Missing context (app required)' },
1197
- });
1198
- return;
1199
- }
1200
- // SECURITY: Merge process.env (baked-in secrets) with request env (API token).
1201
- // This ensures secrets like MAILGUN_API_KEY come from the container,
1202
- // while runtime values like SKEDYUL_API_TOKEN come from the request.
1203
- const mergedEnv = {};
1204
- for (const [key, value] of Object.entries(process.env)) {
1205
- if (value !== undefined) {
1206
- mergedEnv[key] = value;
1207
- }
1208
- }
1209
- // Request env overrides process.env (e.g., for SKEDYUL_API_TOKEN)
1210
- Object.assign(mergedEnv, provisionBody.env ?? {});
1211
- const provisionContext = {
1212
- env: mergedEnv,
1213
- app: provisionBody.context.app,
1214
- };
1215
- // Build request-scoped config for SDK access
1216
- // Use merged env for consistency
1217
- const provisionRequestConfig = {
1218
- baseUrl: mergedEnv.SKEDYUL_API_URL ?? '',
1219
- apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? '',
1220
- };
1221
- try {
1222
- const provisionHook = config.hooks.provision;
1223
- const provisionHandler = typeof provisionHook === 'function'
1224
- ? provisionHook
1225
- : provisionHook.handler;
1226
- const result = await (0, client_1.runWithConfig)(provisionRequestConfig, async () => {
1227
- return await provisionHandler(provisionContext);
1228
- });
1229
- sendJSON(res, 200, result);
1230
- }
1231
- catch (err) {
1232
- sendJSON(res, 500, {
1233
- error: {
1234
- code: -32603,
1235
- message: err instanceof Error ? err.message : String(err ?? ''),
1236
- },
1237
- });
1238
- }
1239
- return;
1240
- }
1241
- if (pathname === '/core' && req.method === 'POST') {
1242
- let coreBody;
1243
- try {
1244
- coreBody = (await parseJSONBody(req));
1245
- }
1246
- catch {
1247
- sendJSON(res, 400, {
1248
- error: {
1249
- code: -32700,
1250
- message: 'Parse error',
1251
- },
1252
- });
1253
- return;
1254
- }
1255
- if (!coreBody?.method) {
1256
- sendJSON(res, 400, {
1257
- error: {
1258
- code: -32602,
1259
- message: 'Missing method',
1260
- },
1261
- });
1262
- return;
1263
- }
1264
- const method = coreBody.method;
1265
- const result = await handleCoreMethod(method, coreBody.params);
1266
- sendCoreResult(result);
1267
- return;
1268
- }
1269
- if (pathname === '/core/webhook' && req.method === 'POST') {
1270
- let rawWebhookBody;
1271
- try {
1272
- rawWebhookBody = await readRawRequestBody(req);
1273
- }
1274
- catch {
1275
- sendJSON(res, 400, { status: 'parse-error' });
1276
- return;
1277
- }
1278
- let webhookBody;
1279
- try {
1280
- webhookBody = rawWebhookBody ? JSON.parse(rawWebhookBody) : {};
1281
- }
1282
- catch {
1283
- sendJSON(res, 400, { status: 'parse-error' });
1284
- return;
1285
- }
1286
- const normalizedHeaders = Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
1287
- key,
1288
- typeof value === 'string' ? value : value?.[0] ?? '',
1289
- ]));
1290
- const coreWebhookRequest = {
1291
- method: req.method ?? 'POST',
1292
- headers: normalizedHeaders,
1293
- body: webhookBody,
1294
- query: Object.fromEntries(url.searchParams.entries()),
1295
- url: url.toString(),
1296
- path: url.pathname,
1297
- rawBody: rawWebhookBody
1298
- ? Buffer.from(rawWebhookBody, 'utf-8')
1299
- : undefined,
1300
- };
1301
- const webhookResponse = await service_1.coreApiService.dispatchWebhook(coreWebhookRequest);
1302
- res.writeHead(webhookResponse.status, {
1303
- 'Content-Type': 'application/json',
1304
- });
1305
- res.end(JSON.stringify(webhookResponse.body ?? {}));
1306
- return;
1307
- }
1308
- if (pathname === '/mcp' && req.method === 'POST') {
1309
- try {
1310
- const body = await parseJSONBody(req);
1311
- // Handle tools/list directly to include custom metadata (timeout, displayName, outputSchema)
1312
- // The MCP SDK only returns standard fields, so we intercept and return the full metadata
1313
- if (body?.method === 'tools/list') {
1314
- sendJSON(res, 200, {
1315
- jsonrpc: '2.0',
1316
- id: body.id ?? null,
1317
- result: { tools },
1318
- });
1319
- return;
1320
- }
1321
- // Handle webhooks/list before passing to MCP SDK transport
1322
- if (body?.method === 'webhooks/list') {
1323
- const webhooks = webhookRegistry
1324
- ? Object.values(webhookRegistry).map((w) => ({
1325
- name: w.name,
1326
- description: w.description,
1327
- methods: w.methods ?? ['POST'],
1328
- type: w.type ?? 'WEBHOOK',
1329
- }))
1330
- : [];
1331
- sendJSON(res, 200, {
1332
- jsonrpc: '2.0',
1333
- id: body.id ?? null,
1334
- result: { webhooks },
1335
- });
1336
- return;
1337
- }
1338
- // Pass to MCP SDK transport for standard MCP methods (tools/call, etc.)
1339
- const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
1340
- sessionIdGenerator: undefined,
1341
- enableJsonResponse: true,
1342
- });
1343
- res.on('close', () => {
1344
- transport.close();
1345
- });
1346
- await mcpServer.connect(transport);
1347
- await transport.handleRequest(req, res, body);
1348
- }
1349
- catch (err) {
1350
- sendJSON(res, 500, {
1351
- jsonrpc: '2.0',
1352
- id: null,
1353
- error: {
1354
- code: -32603,
1355
- message: err instanceof Error ? err.message : String(err ?? ''),
1356
- },
1357
- });
1358
- }
1359
- return;
1360
- }
1361
- sendJSON(res, 404, {
1362
- jsonrpc: '2.0',
1363
- id: null,
1364
- error: {
1365
- code: -32601,
1366
- message: 'Not Found',
1367
- },
1368
- });
1369
- }
1370
- catch (err) {
1371
- sendJSON(res, 500, {
1372
- jsonrpc: '2.0',
1373
- id: null,
1374
- error: {
1375
- code: -32603,
1376
- message: err instanceof Error ? err.message : String(err ?? ''),
1377
- },
1378
- });
1379
- }
1380
- });
1381
- return {
1382
- async listen(listenPort) {
1383
- const finalPort = listenPort ?? port;
1384
- return new Promise((resolve, reject) => {
1385
- httpServer.listen(finalPort, () => {
1386
- printStartupLog(config, tools, webhookRegistry, finalPort);
1387
- resolve();
1388
- });
1389
- httpServer.once('error', reject);
1390
- });
1391
- },
1392
- getHealthStatus: () => state.getHealthStatus(),
1393
- };
1394
- }
1395
- function createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry) {
1396
- const headers = getDefaultHeaders(config.cors);
1397
- // Print startup log once on cold start
1398
- let hasLoggedStartup = false;
1399
- return {
1400
- async handler(event) {
1401
- // Log startup info on first invocation (cold start)
1402
- if (!hasLoggedStartup) {
1403
- printStartupLog(config, tools, webhookRegistry);
1404
- hasLoggedStartup = true;
1405
- }
1406
- try {
1407
- const path = event.path;
1408
- const method = event.httpMethod;
1409
- if (method === 'OPTIONS') {
1410
- return createResponse(200, { message: 'OK' }, headers);
1411
- }
1412
- // Handle webhook requests: /webhooks/{handle}
1413
- if (path.startsWith('/webhooks/') && webhookRegistry) {
1414
- const handle = path.slice('/webhooks/'.length);
1415
- const webhookDef = webhookRegistry[handle];
1416
- if (!webhookDef) {
1417
- return createResponse(404, { error: `Webhook handler '${handle}' not found` }, headers);
1418
- }
1419
- // Check if HTTP method is allowed
1420
- const allowedMethods = webhookDef.methods ?? ['POST'];
1421
- if (!allowedMethods.includes(method)) {
1422
- return createResponse(405, { error: `Method ${method} not allowed` }, headers);
1423
- }
1424
- // Get raw body
1425
- const rawBody = event.body ?? '';
1426
- // Parse body based on content type
1427
- let parsedBody;
1428
- const contentType = event.headers?.['content-type'] ?? event.headers?.['Content-Type'] ?? '';
1429
- if (contentType.includes('application/json')) {
1430
- try {
1431
- parsedBody = rawBody ? JSON.parse(rawBody) : {};
1432
- }
1433
- catch {
1434
- parsedBody = rawBody;
1435
- }
1436
- }
1437
- else {
1438
- parsedBody = rawBody;
1439
- }
1440
- // Check if this is an envelope format from the platform
1441
- // Envelope format: { env: {...}, request: {...}, context: {...} }
1442
- const isEnvelope = (typeof parsedBody === 'object' &&
1443
- parsedBody !== null &&
1444
- 'env' in parsedBody &&
1445
- 'request' in parsedBody &&
1446
- 'context' in parsedBody);
1447
- let webhookRequest;
1448
- let webhookContext;
1449
- let requestEnv = {};
1450
- if (isEnvelope) {
1451
- // Platform envelope format - extract env, request, and context
1452
- const envelope = parsedBody;
1453
- requestEnv = envelope.env ?? {};
1454
- // Parse the original request body
1455
- let originalParsedBody = envelope.request.body;
1456
- const originalContentType = envelope.request.headers['content-type'] ?? '';
1457
- if (originalContentType.includes('application/json')) {
1458
- try {
1459
- originalParsedBody = envelope.request.body ? JSON.parse(envelope.request.body) : {};
1460
- }
1461
- catch {
1462
- // Keep as string if JSON parsing fails
1463
- }
1464
- }
1465
- webhookRequest = {
1466
- method: envelope.request.method,
1467
- url: envelope.request.url,
1468
- path: envelope.request.path,
1469
- headers: envelope.request.headers,
1470
- query: envelope.request.query,
1471
- body: originalParsedBody,
1472
- rawBody: envelope.request.body ? Buffer.from(envelope.request.body, 'utf-8') : undefined,
1473
- };
1474
- const envVars = { ...process.env, ...requestEnv };
1475
- const app = envelope.context.app;
1476
- // Build webhook context based on whether we have installation context
1477
- if (envelope.context.appInstallationId && envelope.context.workplace) {
1478
- // Runtime webhook context
1479
- webhookContext = {
1480
- env: envVars,
1481
- app,
1482
- appInstallationId: envelope.context.appInstallationId,
1483
- workplace: envelope.context.workplace,
1484
- registration: envelope.context.registration ?? {},
1485
- };
1486
- }
1487
- else {
1488
- // Provision webhook context
1489
- webhookContext = {
1490
- env: envVars,
1491
- app,
1492
- };
1493
- }
1494
- }
1495
- else {
1496
- // Direct request format (legacy or direct calls) - requires app info from headers or fail
1497
- const appId = event.headers?.['x-skedyul-app-id'] ?? event.headers?.['X-Skedyul-App-Id'];
1498
- const appVersionId = event.headers?.['x-skedyul-app-version-id'] ?? event.headers?.['X-Skedyul-App-Version-Id'];
1499
- if (!appId || !appVersionId) {
1500
- throw new Error('Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)');
1501
- }
1502
- const forwardedProto = event.headers?.['x-forwarded-proto'] ??
1503
- event.headers?.['X-Forwarded-Proto'];
1504
- const protocol = forwardedProto ?? 'https';
1505
- const host = event.headers?.host ?? event.headers?.Host ?? 'localhost';
1506
- const queryString = event.queryStringParameters
1507
- ? '?' + new URLSearchParams(event.queryStringParameters).toString()
1508
- : '';
1509
- const webhookUrl = `${protocol}://${host}${path}${queryString}`;
1510
- webhookRequest = {
1511
- method,
1512
- url: webhookUrl,
1513
- path,
1514
- headers: event.headers,
1515
- query: event.queryStringParameters ?? {},
1516
- body: parsedBody,
1517
- rawBody: rawBody ? Buffer.from(rawBody, 'utf-8') : undefined,
1518
- };
1519
- // Direct calls are provision-level (no installation context)
1520
- webhookContext = {
1521
- env: process.env,
1522
- app: { id: appId, versionId: appVersionId },
1523
- };
1524
- }
1525
- // Temporarily inject env into process.env for skedyul client to use
1526
- // (same pattern as tool handler)
1527
- const originalEnv = { ...process.env };
1528
- Object.assign(process.env, requestEnv);
1529
- // Build request-scoped config for the skedyul client
1530
- // This uses AsyncLocalStorage to override the global config (same pattern as tools)
1531
- const requestConfig = {
1532
- baseUrl: requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? '',
1533
- apiToken: requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? '',
1534
- };
1535
- // Invoke the handler with request-scoped config
1536
- let webhookResponse;
1537
- try {
1538
- webhookResponse = await (0, client_1.runWithConfig)(requestConfig, async () => {
1539
- return await webhookDef.handler(webhookRequest, webhookContext);
1540
- });
1541
- }
1542
- catch (err) {
1543
- console.error(`Webhook handler '${handle}' error:`, err);
1544
- return createResponse(500, { error: 'Webhook handler error' }, headers);
1545
- }
1546
- finally {
1547
- // Restore original env
1548
- process.env = originalEnv;
1549
- }
1550
- // Build response headers
1551
- const responseHeaders = {
1552
- ...headers,
1553
- ...webhookResponse.headers,
1554
- };
1555
- const status = webhookResponse.status ?? 200;
1556
- const body = webhookResponse.body;
1557
- return {
1558
- statusCode: status,
1559
- headers: responseHeaders,
1560
- body: body !== undefined
1561
- ? (typeof body === 'string' ? body : JSON.stringify(body))
1562
- : '',
1563
- };
1564
- }
1565
- if (path === '/core' && method === 'POST') {
1566
- let coreBody;
1567
- try {
1568
- coreBody = event.body ? JSON.parse(event.body) : {};
1569
- }
1570
- catch {
1571
- return createResponse(400, {
1572
- error: {
1573
- code: -32700,
1574
- message: 'Parse error',
1575
- },
1576
- }, headers);
1577
- }
1578
- if (!coreBody?.method) {
1579
- return createResponse(400, {
1580
- error: {
1581
- code: -32602,
1582
- message: 'Missing method',
1583
- },
1584
- }, headers);
1585
- }
1586
- const method = coreBody.method;
1587
- const result = await handleCoreMethod(method, coreBody.params);
1588
- return createResponse(result.status, result.payload, headers);
1589
- }
1590
- if (path === '/core/webhook' && method === 'POST') {
1591
- const rawWebhookBody = event.body ?? '';
1592
- let webhookBody;
1593
- try {
1594
- webhookBody = rawWebhookBody ? JSON.parse(rawWebhookBody) : {};
1595
- }
1596
- catch {
1597
- return createResponse(400, { status: 'parse-error' }, headers);
1598
- }
1599
- const forwardedProto = event.headers?.['x-forwarded-proto'] ??
1600
- event.headers?.['X-Forwarded-Proto'];
1601
- const protocol = forwardedProto ?? 'https';
1602
- const host = event.headers?.host ?? event.headers?.Host ?? 'localhost';
1603
- const webhookUrl = `${protocol}://${host}${event.path}`;
1604
- const coreWebhookRequest = {
1605
- method,
1606
- headers: (event.headers ?? {}),
1607
- body: webhookBody,
1608
- query: event.queryStringParameters ?? {},
1609
- url: webhookUrl,
1610
- path: event.path,
1611
- rawBody: rawWebhookBody
1612
- ? Buffer.from(rawWebhookBody, 'utf-8')
1613
- : undefined,
1614
- };
1615
- const webhookResponse = await service_1.coreApiService.dispatchWebhook(coreWebhookRequest);
1616
- return createResponse(webhookResponse.status, webhookResponse.body ?? {}, headers);
1617
- }
1618
- if (path === '/estimate' && method === 'POST') {
1619
- let estimateBody;
1620
- try {
1621
- estimateBody = event.body ? JSON.parse(event.body) : {};
1622
- }
1623
- catch {
1624
- return createResponse(400, {
1625
- error: {
1626
- code: -32700,
1627
- message: 'Parse error',
1628
- },
1629
- }, headers);
1630
- }
1631
- try {
1632
- const toolName = estimateBody.name;
1633
- const toolArgs = estimateBody.inputs ?? {};
1634
- // Find tool by name
1635
- let toolKey = null;
1636
- let tool = null;
1637
- for (const [key, t] of Object.entries(registry)) {
1638
- if (t.name === toolName || key === toolName) {
1639
- toolKey = key;
1640
- tool = t;
1641
- break;
1642
- }
1643
- }
1644
- if (!tool || !toolKey) {
1645
- return createResponse(400, {
1646
- error: {
1647
- code: -32602,
1648
- message: `Tool "${toolName}" not found`,
1649
- },
1650
- }, headers);
1651
- }
1652
- const inputSchema = getZodSchema(tool.inputSchema);
1653
- // Validate arguments against Zod schema
1654
- const validatedArgs = inputSchema ? inputSchema.parse(toolArgs) : toolArgs;
1655
- const estimateResponse = await callTool(toolKey, {
1656
- inputs: validatedArgs,
1657
- estimate: true,
1658
- });
1659
- return createResponse(200, {
1660
- billing: estimateResponse.billing ?? { credits: 0 },
1661
- }, headers);
1662
- }
1663
- catch (err) {
1664
- return createResponse(500, {
1665
- error: {
1666
- code: -32603,
1667
- message: err instanceof Error ? err.message : String(err ?? ''),
1668
- },
1669
- }, headers);
1670
- }
1671
- }
1672
- // Handle /install endpoint for install handlers
1673
- if (path === '/install' && method === 'POST') {
1674
- if (!config.hooks?.install) {
1675
- return createResponse(404, { error: 'Install handler not configured' }, headers);
1676
- }
1677
- let installBody;
1678
- try {
1679
- installBody = event.body ? JSON.parse(event.body) : {};
1680
- }
1681
- catch {
1682
- return createResponse(400, { error: { code: -32700, message: 'Parse error' } }, headers);
1683
- }
1684
- if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
1685
- return createResponse(400, { error: { code: -32602, message: 'Missing context (appInstallationId and workplace required)' } }, headers);
1686
- }
1687
- const installContext = {
1688
- env: installBody.env ?? {},
1689
- workplace: installBody.context.workplace,
1690
- appInstallationId: installBody.context.appInstallationId,
1691
- app: installBody.context.app,
1692
- };
1693
- // Build request-scoped config for SDK access
1694
- // Use env from request body (contains generated token from workflow)
1695
- const installRequestConfig = {
1696
- baseUrl: installBody.env?.SKEDYUL_API_URL ??
1697
- process.env.SKEDYUL_API_URL ??
1698
- '',
1699
- apiToken: installBody.env?.SKEDYUL_API_TOKEN ??
1700
- process.env.SKEDYUL_API_TOKEN ??
1701
- '',
1702
- };
1703
- try {
1704
- const installHook = config.hooks.install;
1705
- const installHandler = typeof installHook === 'function'
1706
- ? installHook
1707
- : installHook.handler;
1708
- const result = await (0, client_1.runWithConfig)(installRequestConfig, async () => {
1709
- return await installHandler(installContext);
1710
- });
1711
- return createResponse(200, { env: result.env ?? {}, redirect: result.redirect }, headers);
1712
- }
1713
- catch (err) {
1714
- // Check for typed install errors
1715
- if (err instanceof errors_1.InstallError) {
1716
- return createResponse(400, {
1717
- error: {
1718
- code: err.code,
1719
- message: err.message,
1720
- field: err.field,
1721
- },
1722
- }, headers);
1723
- }
1724
- return createResponse(500, {
1725
- error: {
1726
- code: -32603,
1727
- message: err instanceof Error ? err.message : String(err ?? ''),
1728
- },
1729
- }, headers);
1730
- }
1731
- }
1732
- // Handle /uninstall endpoint for uninstall handlers
1733
- if (path === '/uninstall' && method === 'POST') {
1734
- if (!config.hooks?.uninstall) {
1735
- return createResponse(404, { error: 'Uninstall handler not configured' }, headers);
1736
- }
1737
- let uninstallBody;
1738
- try {
1739
- uninstallBody = event.body ? JSON.parse(event.body) : {};
1740
- }
1741
- catch {
1742
- return createResponse(400, { error: { code: -32700, message: 'Parse error' } }, headers);
1743
- }
1744
- if (!uninstallBody.context?.appInstallationId ||
1745
- !uninstallBody.context?.workplace ||
1746
- !uninstallBody.context?.app) {
1747
- return createResponse(400, {
1748
- error: {
1749
- code: -32602,
1750
- message: 'Missing context (appInstallationId, workplace and app required)',
1751
- },
1752
- }, headers);
1753
- }
1754
- const uninstallContext = {
1755
- env: uninstallBody.env ?? {},
1756
- workplace: uninstallBody.context.workplace,
1757
- appInstallationId: uninstallBody.context.appInstallationId,
1758
- app: uninstallBody.context.app,
1759
- };
1760
- const uninstallRequestConfig = {
1761
- baseUrl: uninstallBody.env?.SKEDYUL_API_URL ??
1762
- process.env.SKEDYUL_API_URL ??
1763
- '',
1764
- apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ??
1765
- process.env.SKEDYUL_API_TOKEN ??
1766
- '',
1767
- };
1768
- try {
1769
- const uninstallHook = config.hooks.uninstall;
1770
- const uninstallHandlerFn = typeof uninstallHook === 'function' ? uninstallHook : uninstallHook.handler;
1771
- const result = await (0, client_1.runWithConfig)(uninstallRequestConfig, async () => {
1772
- return await uninstallHandlerFn(uninstallContext);
1773
- });
1774
- return createResponse(200, { cleanedWebhookIds: result.cleanedWebhookIds ?? [] }, headers);
1775
- }
1776
- catch (err) {
1777
- return createResponse(500, {
1778
- error: {
1779
- code: -32603,
1780
- message: err instanceof Error ? err.message : String(err ?? ''),
1781
- },
1782
- }, headers);
1783
- }
1784
- }
1785
- // Handle /oauth_callback endpoint for OAuth callbacks (called by platform route)
1786
- if (path === '/oauth_callback' && method === 'POST') {
1787
- if (!config.hooks?.oauth_callback) {
1788
- return createResponse(404, { error: 'OAuth callback handler not configured' }, headers);
1789
- }
1790
- let parsedBody;
1791
- try {
1792
- parsedBody = event.body ? JSON.parse(event.body) : {};
1793
- }
1794
- catch (err) {
1795
- console.error('[OAuth Callback] Failed to parse JSON body:', err);
1796
- return createResponse(400, { error: { code: -32700, message: 'Parse error' } }, headers);
1797
- }
1798
- // Debug: Log what we received
1799
- console.log('[OAuth Callback] Parsed body type:', typeof parsedBody);
1800
- console.log('[OAuth Callback] Parsed body is array:', Array.isArray(parsedBody));
1801
- console.log('[OAuth Callback] Parsed body has env:', parsedBody && typeof parsedBody === 'object' && 'env' in parsedBody);
1802
- console.log('[OAuth Callback] Parsed body has request:', parsedBody && typeof parsedBody === 'object' && 'request' in parsedBody);
1803
- if (parsedBody && typeof parsedBody === 'object' && !Array.isArray(parsedBody)) {
1804
- console.log('[OAuth Callback] Parsed body keys:', Object.keys(parsedBody));
1805
- }
1806
- // Parse envelope using shared helper
1807
- const envelope = parseHandlerEnvelope(parsedBody);
1808
- if (!envelope) {
1809
- console.error('[OAuth Callback] Failed to parse envelope. Body:', JSON.stringify(parsedBody, null, 2));
1810
- return createResponse(400, { error: { code: -32602, message: 'Missing envelope format: expected { env, request }' } }, headers);
1811
- }
1812
- // Convert raw request to rich request using shared helper
1813
- const oauthRequest = buildRequestFromRaw(envelope.request);
1814
- // Build request-scoped config using shared helper
1815
- const oauthCallbackRequestConfig = buildRequestScopedConfig(envelope.env);
1816
- const oauthCallbackContext = {
1817
- request: oauthRequest,
1818
- };
1819
- try {
1820
- const oauthCallbackHook = config.hooks.oauth_callback;
1821
- const oauthCallbackHandler = typeof oauthCallbackHook === 'function'
1822
- ? oauthCallbackHook
1823
- : oauthCallbackHook.handler;
1824
- const result = await (0, client_1.runWithConfig)(oauthCallbackRequestConfig, async () => {
1825
- return await oauthCallbackHandler(oauthCallbackContext);
1826
- });
1827
- return createResponse(200, {
1828
- appInstallationId: result.appInstallationId,
1829
- env: result.env ?? {},
1830
- }, headers);
1831
- }
1832
- catch (err) {
1833
- const errorMessage = err instanceof Error ? err.message : String(err ?? 'Unknown error');
1834
- return createResponse(500, {
1835
- error: {
1836
- code: -32603,
1837
- message: errorMessage,
1838
- },
1839
- }, headers);
1840
- }
1841
- }
1842
- if (path === '/health' && method === 'GET') {
1843
- return createResponse(200, state.getHealthStatus(), headers);
1844
- }
1845
- if (path === '/mcp' && method === 'POST') {
1846
- let body;
1847
- try {
1848
- body = event.body ? JSON.parse(event.body) : {};
1849
- }
1850
- catch {
1851
- return createResponse(400, {
1852
- jsonrpc: '2.0',
1853
- id: null,
1854
- error: {
1855
- code: -32700,
1856
- message: 'Parse error',
1857
- },
1858
- }, headers);
1859
- }
1860
- try {
1861
- const { jsonrpc, id, method: rpcMethod, params } = body;
1862
- if (jsonrpc !== '2.0') {
1863
- return createResponse(400, {
1864
- jsonrpc: '2.0',
1865
- id,
1866
- error: {
1867
- code: -32600,
1868
- message: 'Invalid Request',
1869
- },
1870
- }, headers);
1871
- }
1872
- let result;
1873
- if (rpcMethod === 'tools/list') {
1874
- result = { tools };
1875
- }
1876
- else if (rpcMethod === 'tools/call') {
1877
- const toolName = params?.name;
1878
- // Support both formats:
1879
- // 1. Skedyul format: { inputs: {...}, context: {...}, env: {...} }
1880
- // 2. Standard MCP format: { ...directArgs }
1881
- const rawArgs = (params?.arguments ?? {});
1882
- const hasSkedyulFormat = 'inputs' in rawArgs || 'env' in rawArgs || 'context' in rawArgs;
1883
- const toolInputs = hasSkedyulFormat ? (rawArgs.inputs ?? {}) : rawArgs;
1884
- const toolContext = hasSkedyulFormat ? rawArgs.context : undefined;
1885
- const toolEnv = hasSkedyulFormat ? rawArgs.env : undefined;
1886
- // Debug logging for MCP tool calls
1887
- console.log('\n📞 MCP tools/call received:');
1888
- console.log(' Tool:', toolName);
1889
- console.log(' Raw arguments:', JSON.stringify(rawArgs, null, 2));
1890
- console.log(' Skedyul format detected:', hasSkedyulFormat);
1891
- console.log(' Extracted inputs:', JSON.stringify(toolInputs, null, 2));
1892
- console.log(' Extracted context:', JSON.stringify(toolContext, null, 2));
1893
- console.log(' Extracted env keys:', toolEnv ? Object.keys(toolEnv) : 'none');
1894
- // Find tool by name (check both registry key and tool.name)
1895
- let toolKey = null;
1896
- let tool = null;
1897
- for (const [key, t] of Object.entries(registry)) {
1898
- if (t.name === toolName || key === toolName) {
1899
- toolKey = key;
1900
- tool = t;
1901
- break;
1902
- }
1903
- }
1904
- if (!tool || !toolKey) {
1905
- return createResponse(200, {
1906
- jsonrpc: '2.0',
1907
- id,
1908
- error: {
1909
- code: -32602,
1910
- message: `Tool "${toolName}" not found`,
1911
- },
1912
- }, headers);
1913
- }
1914
- try {
1915
- const inputSchema = getZodSchema(tool.inputSchema);
1916
- const outputSchema = getZodSchema(tool.outputSchema);
1917
- const hasOutputSchema = Boolean(outputSchema);
1918
- const validatedInputs = inputSchema
1919
- ? inputSchema.parse(toolInputs)
1920
- : toolInputs;
1921
- const toolResult = await callTool(toolKey, {
1922
- inputs: validatedInputs,
1923
- context: toolContext,
1924
- env: toolEnv,
1925
- });
1926
- // Transform internal format to MCP protocol format
1927
- // Note: effect is embedded in structuredContent as __effect
1928
- // for consistency with dedicated mode (MCP SDK strips custom fields)
1929
- if (toolResult.error) {
1930
- const errorOutput = { error: toolResult.error };
1931
- result = {
1932
- content: [{ type: 'text', text: JSON.stringify(errorOutput) }],
1933
- structuredContent: errorOutput,
1934
- isError: true,
1935
- billing: toolResult.billing,
1936
- };
1937
- }
1938
- else {
1939
- const outputData = toolResult.output;
1940
- const structuredContent = outputData
1941
- ? { ...outputData, __effect: toolResult.effect }
1942
- : toolResult.effect
1943
- ? { __effect: toolResult.effect }
1944
- : undefined;
1945
- result = {
1946
- content: [{ type: 'text', text: JSON.stringify(toolResult.output) }],
1947
- structuredContent,
1948
- billing: toolResult.billing,
1949
- };
1950
- }
1951
- }
1952
- catch (validationError) {
1953
- return createResponse(200, {
1954
- jsonrpc: '2.0',
1955
- id,
1956
- error: {
1957
- code: -32602,
1958
- message: validationError instanceof Error
1959
- ? validationError.message
1960
- : 'Invalid arguments',
1961
- },
1962
- }, headers);
1963
- }
1964
- }
1965
- else if (rpcMethod === 'webhooks/list') {
1966
- // Return registered webhooks with their metadata
1967
- const webhooks = webhookRegistry
1968
- ? Object.values(webhookRegistry).map((w) => ({
1969
- name: w.name,
1970
- description: w.description,
1971
- methods: w.methods ?? ['POST'],
1972
- type: w.type ?? 'WEBHOOK',
1973
- }))
1974
- : [];
1975
- result = { webhooks };
1976
- }
1977
- else {
1978
- return createResponse(200, {
1979
- jsonrpc: '2.0',
1980
- id,
1981
- error: {
1982
- code: -32601,
1983
- message: `Method not found: ${rpcMethod}`,
1984
- },
1985
- }, headers);
1986
- }
1987
- return createResponse(200, {
1988
- jsonrpc: '2.0',
1989
- id,
1990
- result,
1991
- }, headers);
1992
- }
1993
- catch (err) {
1994
- return createResponse(500, {
1995
- jsonrpc: '2.0',
1996
- id: body?.id ?? null,
1997
- error: {
1998
- code: -32603,
1999
- message: err instanceof Error ? err.message : String(err ?? ''),
2000
- },
2001
- }, headers);
2002
- }
2003
- }
2004
- return createResponse(404, {
2005
- jsonrpc: '2.0',
2006
- id: null,
2007
- error: {
2008
- code: -32601,
2009
- message: 'Not Found',
2010
- },
2011
- }, headers);
2012
- }
2013
- catch (err) {
2014
- return createResponse(500, {
2015
- jsonrpc: '2.0',
2016
- id: null,
2017
- error: {
2018
- code: -32603,
2019
- message: err instanceof Error ? err.message : String(err ?? ''),
2020
- },
2021
- }, headers);
2022
- }
2023
- },
2024
- getHealthStatus: () => state.getHealthStatus(),
2025
- };
2026
- }
2027
- exports.server = {
2028
- create: createSkedyulServer,
2029
- };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createServerlessInstance = exports.createDedicatedServerInstance = exports.padEnd = exports.printStartupLog = exports.buildRequestScopedConfig = exports.buildRequestFromRaw = exports.parseHandlerEnvelope = exports.createCallToolHandler = exports.createRequestState = exports.buildToolMetadata = exports.handleCoreMethod = exports.getListeningPort = exports.createResponse = exports.getDefaultHeaders = exports.sendHTML = exports.sendJSON = exports.parseJSONBody = exports.readRawRequestBody = exports.mergeRuntimeEnv = exports.parseNumberEnv = exports.parseJsonRecord = exports.getJsonSchemaFromToolSchema = exports.getZodSchema = exports.isToolSchemaWithJson = exports.toJsonSchema = exports.normalizeBilling = exports.server = exports.createSkedyulServer = void 0;
10
+ // Re-export everything from the server module
11
+ var index_1 = require("./server/index");
12
+ // Main factory function
13
+ Object.defineProperty(exports, "createSkedyulServer", { enumerable: true, get: function () { return index_1.createSkedyulServer; } });
14
+ Object.defineProperty(exports, "server", { enumerable: true, get: function () { return index_1.server; } });
15
+ // Utilities
16
+ Object.defineProperty(exports, "normalizeBilling", { enumerable: true, get: function () { return index_1.normalizeBilling; } });
17
+ Object.defineProperty(exports, "toJsonSchema", { enumerable: true, get: function () { return index_1.toJsonSchema; } });
18
+ Object.defineProperty(exports, "isToolSchemaWithJson", { enumerable: true, get: function () { return index_1.isToolSchemaWithJson; } });
19
+ Object.defineProperty(exports, "getZodSchema", { enumerable: true, get: function () { return index_1.getZodSchema; } });
20
+ Object.defineProperty(exports, "getJsonSchemaFromToolSchema", { enumerable: true, get: function () { return index_1.getJsonSchemaFromToolSchema; } });
21
+ Object.defineProperty(exports, "parseJsonRecord", { enumerable: true, get: function () { return index_1.parseJsonRecord; } });
22
+ Object.defineProperty(exports, "parseNumberEnv", { enumerable: true, get: function () { return index_1.parseNumberEnv; } });
23
+ Object.defineProperty(exports, "mergeRuntimeEnv", { enumerable: true, get: function () { return index_1.mergeRuntimeEnv; } });
24
+ Object.defineProperty(exports, "readRawRequestBody", { enumerable: true, get: function () { return index_1.readRawRequestBody; } });
25
+ Object.defineProperty(exports, "parseJSONBody", { enumerable: true, get: function () { return index_1.parseJSONBody; } });
26
+ Object.defineProperty(exports, "sendJSON", { enumerable: true, get: function () { return index_1.sendJSON; } });
27
+ Object.defineProperty(exports, "sendHTML", { enumerable: true, get: function () { return index_1.sendHTML; } });
28
+ Object.defineProperty(exports, "getDefaultHeaders", { enumerable: true, get: function () { return index_1.getDefaultHeaders; } });
29
+ Object.defineProperty(exports, "createResponse", { enumerable: true, get: function () { return index_1.createResponse; } });
30
+ Object.defineProperty(exports, "getListeningPort", { enumerable: true, get: function () { return index_1.getListeningPort; } });
31
+ // Handlers
32
+ Object.defineProperty(exports, "handleCoreMethod", { enumerable: true, get: function () { return index_1.handleCoreMethod; } });
33
+ Object.defineProperty(exports, "buildToolMetadata", { enumerable: true, get: function () { return index_1.buildToolMetadata; } });
34
+ Object.defineProperty(exports, "createRequestState", { enumerable: true, get: function () { return index_1.createRequestState; } });
35
+ Object.defineProperty(exports, "createCallToolHandler", { enumerable: true, get: function () { return index_1.createCallToolHandler; } });
36
+ Object.defineProperty(exports, "parseHandlerEnvelope", { enumerable: true, get: function () { return index_1.parseHandlerEnvelope; } });
37
+ Object.defineProperty(exports, "buildRequestFromRaw", { enumerable: true, get: function () { return index_1.buildRequestFromRaw; } });
38
+ Object.defineProperty(exports, "buildRequestScopedConfig", { enumerable: true, get: function () { return index_1.buildRequestScopedConfig; } });
39
+ Object.defineProperty(exports, "printStartupLog", { enumerable: true, get: function () { return index_1.printStartupLog; } });
40
+ Object.defineProperty(exports, "padEnd", { enumerable: true, get: function () { return index_1.padEnd; } });
41
+ Object.defineProperty(exports, "createDedicatedServerInstance", { enumerable: true, get: function () { return index_1.createDedicatedServerInstance; } });
42
+ Object.defineProperty(exports, "createServerlessInstance", { enumerable: true, get: function () { return index_1.createServerlessInstance; } });