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.
- package/dist/.build-stamp +1 -1
- package/dist/config/app-config.d.ts +73 -0
- package/dist/config/app-config.js +12 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +33 -0
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +119 -0
- package/dist/config/types/agent.d.ts +29 -0
- package/dist/config/types/agent.js +5 -0
- package/dist/config/types/channel.d.ts +46 -0
- package/dist/config/types/channel.js +2 -0
- package/dist/config/types/compute.d.ts +1 -0
- package/dist/config/types/compute.js +5 -0
- package/dist/config/types/env.d.ts +16 -0
- package/dist/config/types/env.js +5 -0
- package/dist/config/types/index.d.ts +9 -0
- package/dist/config/types/index.js +26 -0
- package/dist/config/types/model.d.ts +62 -0
- package/dist/config/types/model.js +2 -0
- package/dist/config/types/page.d.ts +436 -0
- package/dist/config/types/page.js +5 -0
- package/dist/config/types/resource.d.ts +30 -0
- package/dist/config/types/resource.js +5 -0
- package/dist/config/types/webhook.d.ts +35 -0
- package/dist/config/types/webhook.js +5 -0
- package/dist/config/types/workflow.d.ts +24 -0
- package/dist/config/types/workflow.js +2 -0
- package/dist/config/utils.d.ts +16 -0
- package/dist/config/utils.js +37 -0
- package/dist/config.d.ts +5 -767
- package/dist/config.js +11 -151
- package/dist/schemas.d.ts +43 -43
- package/dist/server/core-api-handler.d.ts +8 -0
- package/dist/server/core-api-handler.js +148 -0
- package/dist/server/dedicated.d.ts +7 -0
- package/dist/server/dedicated.js +610 -0
- package/dist/server/handler-helpers.d.ts +24 -0
- package/dist/server/handler-helpers.js +75 -0
- package/dist/server/index.d.ts +19 -0
- package/dist/server/index.js +196 -0
- package/dist/server/serverless.d.ts +7 -0
- package/dist/server/serverless.js +629 -0
- package/dist/server/startup-logger.d.ts +9 -0
- package/dist/server/startup-logger.js +113 -0
- package/dist/server/tool-handler.d.ts +14 -0
- package/dist/server/tool-handler.js +189 -0
- package/dist/server/types.d.ts +22 -0
- package/dist/server/types.js +2 -0
- package/dist/server/utils/env.d.ts +12 -0
- package/dist/server/utils/env.js +38 -0
- package/dist/server/utils/http.d.ts +30 -0
- package/dist/server/utils/http.js +81 -0
- package/dist/server/utils/index.d.ts +3 -0
- package/dist/server/utils/index.js +24 -0
- package/dist/server/utils/schema.d.ts +22 -0
- package/dist/server/utils/schema.js +102 -0
- package/dist/server.d.ts +7 -11
- package/dist/server.js +39 -2026
- package/dist/types/aws.d.ts +15 -0
- package/dist/types/aws.js +5 -0
- package/dist/types/handlers.d.ts +122 -0
- package/dist/types/handlers.js +2 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/index.js +16 -0
- package/dist/types/server.d.ts +43 -0
- package/dist/types/server.js +2 -0
- package/dist/types/shared.d.ts +16 -0
- package/dist/types/shared.js +5 -0
- package/dist/types/tool-context.d.ts +64 -0
- package/dist/types/tool-context.js +12 -0
- package/dist/types/tool.d.ts +96 -0
- package/dist/types/tool.js +19 -0
- package/dist/types/webhook.d.ts +116 -0
- package/dist/types/webhook.js +7 -0
- package/dist/types.d.ts +4 -461
- package/dist/types.js +21 -31
- 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
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
function
|
|
763
|
-
|
|
764
|
-
|
|
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; } });
|