skedyul 0.1.26 → 0.1.28

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/server.js CHANGED
@@ -401,6 +401,110 @@ function getListeningPort(config) {
401
401
  }
402
402
  return config.defaultPort ?? 3000;
403
403
  }
404
+ /**
405
+ * Prints a styled startup log showing server configuration
406
+ */
407
+ function printStartupLog(config, tools, webhookRegistry, port) {
408
+ const webhookCount = webhookRegistry ? Object.keys(webhookRegistry).length : 0;
409
+ const webhookNames = webhookRegistry ? Object.keys(webhookRegistry) : [];
410
+ const maxRequests = config.maxRequests ??
411
+ parseNumberEnv(process.env.MCP_MAX_REQUESTS) ??
412
+ null;
413
+ const ttlExtendSeconds = config.ttlExtendSeconds ??
414
+ parseNumberEnv(process.env.MCP_TTL_EXTEND) ??
415
+ 3600;
416
+ const executableId = process.env.SKEDYUL_EXECUTABLE_ID || 'local';
417
+ const divider = '═'.repeat(70);
418
+ const thinDivider = '─'.repeat(70);
419
+ // eslint-disable-next-line no-console
420
+ console.log('');
421
+ // eslint-disable-next-line no-console
422
+ console.log(`╔${divider}╗`);
423
+ // eslint-disable-next-line no-console
424
+ console.log(`║ 🚀 Skedyul MCP Server Starting ║`);
425
+ // eslint-disable-next-line no-console
426
+ console.log(`╠${divider}╣`);
427
+ // eslint-disable-next-line no-console
428
+ console.log(`║ ║`);
429
+ // eslint-disable-next-line no-console
430
+ console.log(`║ 📦 Server: ${padEnd(config.metadata.name, 49)}║`);
431
+ // eslint-disable-next-line no-console
432
+ console.log(`║ 🏷️ Version: ${padEnd(config.metadata.version, 49)}║`);
433
+ // eslint-disable-next-line no-console
434
+ console.log(`║ ⚡ Compute: ${padEnd(config.computeLayer, 49)}║`);
435
+ if (port) {
436
+ // eslint-disable-next-line no-console
437
+ console.log(`║ 🌐 Port: ${padEnd(String(port), 49)}║`);
438
+ }
439
+ // eslint-disable-next-line no-console
440
+ console.log(`║ 🔑 Executable: ${padEnd(executableId, 49)}║`);
441
+ // eslint-disable-next-line no-console
442
+ console.log(`║ ║`);
443
+ // eslint-disable-next-line no-console
444
+ console.log(`╟${thinDivider}╢`);
445
+ // eslint-disable-next-line no-console
446
+ console.log(`║ ║`);
447
+ // eslint-disable-next-line no-console
448
+ console.log(`║ 🔧 Tools (${tools.length}): ║`);
449
+ // List tools (max 10, then show "and X more...")
450
+ const maxToolsToShow = 10;
451
+ const toolsToShow = tools.slice(0, maxToolsToShow);
452
+ for (const tool of toolsToShow) {
453
+ // eslint-disable-next-line no-console
454
+ console.log(`║ • ${padEnd(tool.name, 61)}║`);
455
+ }
456
+ if (tools.length > maxToolsToShow) {
457
+ // eslint-disable-next-line no-console
458
+ console.log(`║ ... and ${tools.length - maxToolsToShow} more ║`);
459
+ }
460
+ if (webhookCount > 0) {
461
+ // eslint-disable-next-line no-console
462
+ console.log(`║ ║`);
463
+ // eslint-disable-next-line no-console
464
+ console.log(`║ 🪝 Webhooks (${webhookCount}): ║`);
465
+ const maxWebhooksToShow = 5;
466
+ const webhooksToShow = webhookNames.slice(0, maxWebhooksToShow);
467
+ for (const name of webhooksToShow) {
468
+ // eslint-disable-next-line no-console
469
+ console.log(`║ • /webhooks/${padEnd(name, 51)}║`);
470
+ }
471
+ if (webhookCount > maxWebhooksToShow) {
472
+ // eslint-disable-next-line no-console
473
+ console.log(`║ ... and ${webhookCount - maxWebhooksToShow} more ║`);
474
+ }
475
+ }
476
+ // eslint-disable-next-line no-console
477
+ console.log(`║ ║`);
478
+ // eslint-disable-next-line no-console
479
+ console.log(`╟${thinDivider}╢`);
480
+ // eslint-disable-next-line no-console
481
+ console.log(`║ ║`);
482
+ // eslint-disable-next-line no-console
483
+ console.log(`║ ⚙️ Configuration: ║`);
484
+ // eslint-disable-next-line no-console
485
+ console.log(`║ Max Requests: ${padEnd(maxRequests !== null ? String(maxRequests) : 'unlimited', 46)}║`);
486
+ // eslint-disable-next-line no-console
487
+ console.log(`║ TTL Extend: ${padEnd(`${ttlExtendSeconds}s`, 46)}║`);
488
+ // eslint-disable-next-line no-console
489
+ console.log(`║ ║`);
490
+ // eslint-disable-next-line no-console
491
+ console.log(`╟${thinDivider}╢`);
492
+ // eslint-disable-next-line no-console
493
+ console.log(`║ ✅ Ready at ${padEnd(new Date().toISOString(), 55)}║`);
494
+ // eslint-disable-next-line no-console
495
+ console.log(`╚${divider}╝`);
496
+ // eslint-disable-next-line no-console
497
+ console.log('');
498
+ }
499
+ /**
500
+ * Pad a string to the right with spaces
501
+ */
502
+ function padEnd(str, length) {
503
+ if (str.length >= length) {
504
+ return str.slice(0, length);
505
+ }
506
+ return str + ' '.repeat(length - str.length);
507
+ }
404
508
  function createSkedyulServer(config, registry, webhookRegistry) {
405
509
  mergeRuntimeEnv();
406
510
  if (config.coreApi?.service) {
@@ -678,15 +782,32 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
678
782
  return;
679
783
  }
680
784
  if (pathname === '/mcp' && req.method === 'POST') {
681
- const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
682
- sessionIdGenerator: undefined,
683
- enableJsonResponse: true,
684
- });
685
- res.on('close', () => {
686
- transport.close();
687
- });
688
785
  try {
689
786
  const body = await parseJSONBody(req);
787
+ // Handle webhooks/list before passing to MCP SDK transport
788
+ if (body?.method === 'webhooks/list') {
789
+ const webhooks = webhookRegistry
790
+ ? Object.values(webhookRegistry).map((w) => ({
791
+ name: w.name,
792
+ description: w.description,
793
+ methods: w.methods ?? ['POST'],
794
+ }))
795
+ : [];
796
+ sendJSON(res, 200, {
797
+ jsonrpc: '2.0',
798
+ id: body.id ?? null,
799
+ result: { webhooks },
800
+ });
801
+ return;
802
+ }
803
+ // Pass to MCP SDK transport for standard MCP methods
804
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
805
+ sessionIdGenerator: undefined,
806
+ enableJsonResponse: true,
807
+ });
808
+ res.on('close', () => {
809
+ transport.close();
810
+ });
690
811
  await mcpServer.connect(transport);
691
812
  await transport.handleRequest(req, res, body);
692
813
  }
@@ -727,12 +848,7 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
727
848
  const finalPort = listenPort ?? port;
728
849
  return new Promise((resolve, reject) => {
729
850
  httpServer.listen(finalPort, () => {
730
- // eslint-disable-next-line no-console
731
- console.log(`MCP Server running on port ${finalPort}`);
732
- // eslint-disable-next-line no-console
733
- console.log(`Registry loaded with ${tools.length} tools: ${tools
734
- .map((tool) => tool.name)
735
- .join(', ')}`);
851
+ printStartupLog(config, tools, webhookRegistry, finalPort);
736
852
  resolve();
737
853
  });
738
854
  httpServer.once('error', reject);
@@ -743,8 +859,15 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
743
859
  }
744
860
  function createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry) {
745
861
  const headers = getDefaultHeaders(config.cors);
862
+ // Print startup log once on cold start
863
+ let hasLoggedStartup = false;
746
864
  return {
747
865
  async handler(event) {
866
+ // Log startup info on first invocation (cold start)
867
+ if (!hasLoggedStartup) {
868
+ printStartupLog(config, tools, webhookRegistry);
869
+ hasLoggedStartup = true;
870
+ }
748
871
  try {
749
872
  const path = event.path;
750
873
  const method = event.httpMethod;
@@ -1039,6 +1162,17 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
1039
1162
  }, headers);
1040
1163
  }
1041
1164
  }
1165
+ else if (rpcMethod === 'webhooks/list') {
1166
+ // Return registered webhooks with their metadata
1167
+ const webhooks = webhookRegistry
1168
+ ? Object.values(webhookRegistry).map((w) => ({
1169
+ name: w.name,
1170
+ description: w.description,
1171
+ methods: w.methods ?? ['POST'],
1172
+ }))
1173
+ : [];
1174
+ result = { webhooks };
1175
+ }
1042
1176
  else {
1043
1177
  return createResponse(200, {
1044
1178
  jsonrpc: '2.0',
package/dist/types.d.ts CHANGED
@@ -132,12 +132,56 @@ export interface WebhookContext {
132
132
  env: Record<string, string | undefined>;
133
133
  }
134
134
  export type WebhookHandler = (request: WebhookRequest, context: WebhookContext) => Promise<WebhookResponse> | WebhookResponse;
135
+ export interface WebhookLifecycleContext {
136
+ /** The Skedyul-generated webhook URL for this webhook */
137
+ webhookUrl: string;
138
+ /** Environment variables available during lifecycle operation */
139
+ env: Record<string, string | undefined>;
140
+ }
141
+ export interface CommunicationChannelLifecycleContext extends WebhookLifecycleContext {
142
+ /** The communication channel being configured */
143
+ communicationChannel: {
144
+ id: string;
145
+ /** The identifier value (e.g., phone number like "+15551234567") */
146
+ identifierValue: string;
147
+ /** The channel handle (e.g., "sms") */
148
+ handle: string;
149
+ };
150
+ }
151
+ export interface WebhookLifecycleResult {
152
+ /** External ID from the provider (e.g., Twilio phone number SID) */
153
+ externalId: string;
154
+ /** Optional message describing what was configured */
155
+ message?: string;
156
+ /** Optional metadata from the provider */
157
+ metadata?: Record<string, unknown>;
158
+ }
159
+ /**
160
+ * Lifecycle hook for webhook operations.
161
+ * Return null if the API doesn't support programmatic management
162
+ * (user must configure manually).
163
+ */
164
+ export type WebhookLifecycleHook<TContext = WebhookLifecycleContext> = (context: TContext) => Promise<WebhookLifecycleResult | null> | WebhookLifecycleResult | null;
135
165
  export interface WebhookDefinition {
136
166
  name: string;
137
167
  description: string;
138
168
  /** HTTP methods this webhook accepts. Defaults to ['POST'] */
139
169
  methods?: ('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH')[];
140
170
  handler: WebhookHandler;
171
+ /** Called when the app is installed to a workplace. Return null if manual setup required. */
172
+ onAppInstalled?: WebhookLifecycleHook;
173
+ /** Called when the app is uninstalled from a workplace. Return null if manual cleanup required. */
174
+ onAppUninstalled?: WebhookLifecycleHook;
175
+ /** Called when a new app version is provisioned. Return null if manual setup required. */
176
+ onAppVersionProvisioned?: WebhookLifecycleHook;
177
+ /** Called when an app version is deprovisioned. Return null if manual cleanup required. */
178
+ onAppVersionDeprovisioned?: WebhookLifecycleHook;
179
+ /** Called when a communication channel is created. Return null if manual setup required. */
180
+ onCommunicationChannelCreated?: WebhookLifecycleHook<CommunicationChannelLifecycleContext>;
181
+ /** Called when a communication channel is updated. Return null if manual update required. */
182
+ onCommunicationChannelUpdated?: WebhookLifecycleHook<CommunicationChannelLifecycleContext>;
183
+ /** Called when a communication channel is deleted. Return null if manual cleanup required. */
184
+ onCommunicationChannelDeleted?: WebhookLifecycleHook<CommunicationChannelLifecycleContext>;
141
185
  }
142
186
  export type WebhookRegistry = Record<string, WebhookDefinition>;
143
187
  export type WebhookName<T extends WebhookRegistry> = Extract<keyof T, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skedyul",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "The Skedyul SDK for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",