skedyul 1.2.19 → 1.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli/index.js +512 -731
  2. package/dist/config/app-config.d.ts +26 -2
  3. package/dist/config/index.d.ts +1 -2
  4. package/dist/config/types/env.d.ts +8 -2
  5. package/dist/config/types/form.d.ts +10 -6
  6. package/dist/config/types/index.d.ts +0 -1
  7. package/dist/config/types/page.d.ts +2 -2
  8. package/dist/config/types/webhook.d.ts +2 -1
  9. package/dist/dedicated/server.js +503 -766
  10. package/dist/esm/index.mjs +503 -720
  11. package/dist/index.d.ts +2 -3
  12. package/dist/index.js +503 -722
  13. package/dist/server/config-serializer.d.ts +12 -0
  14. package/dist/server/dedicated.d.ts +3 -2
  15. package/dist/server/handlers/index.d.ts +12 -0
  16. package/dist/server/handlers/install-handler.d.ts +9 -0
  17. package/dist/server/handlers/oauth-callback-handler.d.ts +9 -0
  18. package/dist/server/handlers/provision-handler.d.ts +9 -0
  19. package/dist/server/handlers/types.d.ts +101 -0
  20. package/dist/server/handlers/uninstall-handler.d.ts +9 -0
  21. package/dist/server/handlers/webhook-handler.d.ts +28 -0
  22. package/dist/server/index.d.ts +15 -6
  23. package/dist/server/serverless.d.ts +3 -2
  24. package/dist/server/startup-logger.d.ts +3 -2
  25. package/dist/server/tool-handler.d.ts +1 -1
  26. package/dist/server/utils/http.d.ts +3 -2
  27. package/dist/server.d.ts +1 -1
  28. package/dist/server.js +503 -766
  29. package/dist/serverless/server.mjs +503 -744
  30. package/dist/types/handlers.d.ts +6 -24
  31. package/dist/types/index.d.ts +4 -4
  32. package/dist/types/server.d.ts +1 -34
  33. package/dist/types/shared.d.ts +5 -0
  34. package/dist/types/tool.d.ts +5 -7
  35. package/dist/types/webhook.d.ts +14 -6
  36. package/package.json +1 -1
  37. package/dist/config/resolve.d.ts +0 -27
  38. package/dist/config/types/compute.d.ts +0 -9
@@ -142,10 +142,6 @@ function sendJSON(res, statusCode, data) {
142
142
  res.writeHead(statusCode, { "Content-Type": "application/json" });
143
143
  res.end(JSON.stringify(data));
144
144
  }
145
- function sendHTML(res, statusCode, html) {
146
- res.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" });
147
- res.end(html);
148
- }
149
145
  function getDefaultHeaders(options) {
150
146
  return {
151
147
  "Content-Type": "application/json",
@@ -331,8 +327,8 @@ function createRequestState(maxRequests, ttlExtendSeconds, runtimeLabel, toolNam
331
327
  };
332
328
  }
333
329
  function createCallToolHandler(registry, state, onMaxRequests) {
334
- return async function callTool(nameRaw, argsRaw) {
335
- const toolName = String(nameRaw);
330
+ return async function callTool(toolNameInput, toolArgsInput) {
331
+ const toolName = String(toolNameInput);
336
332
  const tool = registry[toolName];
337
333
  if (!tool) {
338
334
  throw new Error(`Tool "${toolName}" not found in registry`);
@@ -341,7 +337,7 @@ function createCallToolHandler(registry, state, onMaxRequests) {
341
337
  throw new Error(`Tool "${toolName}" handler is not a function`);
342
338
  }
343
339
  const fn = tool.handler;
344
- const args = argsRaw ?? {};
340
+ const args = toolArgsInput ?? {};
345
341
  const estimateMode = args.estimate === true;
346
342
  if (!estimateMode) {
347
343
  state.incrementRequestCount();
@@ -596,51 +592,6 @@ async function handleCoreMethod(method, params) {
596
592
  };
597
593
  }
598
594
 
599
- // src/server/handler-helpers.ts
600
- function parseHandlerEnvelope(parsedBody) {
601
- if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody) || !("env" in parsedBody) || !("request" in parsedBody)) {
602
- return null;
603
- }
604
- const envelope = parsedBody;
605
- if (typeof envelope.env !== "object" || envelope.env === null || Array.isArray(envelope.env)) {
606
- return null;
607
- }
608
- if (typeof envelope.request !== "object" || envelope.request === null || Array.isArray(envelope.request)) {
609
- return null;
610
- }
611
- return {
612
- env: envelope.env,
613
- request: envelope.request,
614
- context: envelope.context
615
- };
616
- }
617
- function buildRequestFromRaw(raw) {
618
- let parsedBody = raw.body;
619
- const contentType = raw.headers["content-type"] ?? "";
620
- if (contentType.includes("application/json")) {
621
- try {
622
- parsedBody = raw.body ? JSON.parse(raw.body) : {};
623
- } catch {
624
- parsedBody = raw.body;
625
- }
626
- }
627
- return {
628
- method: raw.method,
629
- url: raw.url,
630
- path: raw.path,
631
- headers: raw.headers,
632
- query: raw.query,
633
- body: parsedBody,
634
- rawBody: raw.body ? Buffer.from(raw.body, "utf-8") : void 0
635
- };
636
- }
637
- function buildRequestScopedConfig(env) {
638
- return {
639
- baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
640
- apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
641
- };
642
- }
643
-
644
595
  // src/server/startup-logger.ts
645
596
  function padEnd(str, length) {
646
597
  if (str.length >= length) {
@@ -648,10 +599,11 @@ function padEnd(str, length) {
648
599
  }
649
600
  return str + " ".repeat(length - str.length);
650
601
  }
651
- function printStartupLog(config, tools, webhookRegistry, port) {
602
+ function printStartupLog(config, tools, port) {
652
603
  if (process.env.NODE_ENV === "test") {
653
604
  return;
654
605
  }
606
+ const webhookRegistry = config.webhooks;
655
607
  const webhookCount = webhookRegistry ? Object.keys(webhookRegistry).length : 0;
656
608
  const webhookNames = webhookRegistry ? Object.keys(webhookRegistry) : [];
657
609
  const maxRequests = config.maxRequests ?? parseNumberEnv(process.env.MCP_MAX_REQUESTS) ?? null;
@@ -664,9 +616,9 @@ function printStartupLog(config, tools, webhookRegistry, port) {
664
616
  console.log(`\u2551 \u{1F680} Skedyul MCP Server Starting \u2551`);
665
617
  console.log(`\u2560${divider}\u2563`);
666
618
  console.log(`\u2551 \u2551`);
667
- console.log(`\u2551 \u{1F4E6} Server: ${padEnd(config.metadata.name, 49)}\u2551`);
668
- console.log(`\u2551 \u{1F3F7}\uFE0F Version: ${padEnd(config.metadata.version, 49)}\u2551`);
669
- console.log(`\u2551 \u26A1 Compute: ${padEnd(config.computeLayer, 49)}\u2551`);
619
+ console.log(`\u2551 \u{1F4E6} Server: ${padEnd(config.name, 49)}\u2551`);
620
+ console.log(`\u2551 \u{1F3F7}\uFE0F Version: ${padEnd(config.version ?? "N/A", 49)}\u2551`);
621
+ console.log(`\u2551 \u26A1 Compute: ${padEnd(config.computeLayer ?? "serverless", 49)}\u2551`);
670
622
  if (port) {
671
623
  console.log(`\u2551 \u{1F310} Port: ${padEnd(String(port), 49)}\u2551`);
672
624
  }
@@ -708,67 +660,439 @@ function printStartupLog(config, tools, webhookRegistry, port) {
708
660
  console.log("");
709
661
  }
710
662
 
711
- // src/config/resolve.ts
712
- async function resolveDynamicImport(value) {
713
- if (value === void 0 || value === null) {
714
- return void 0;
663
+ // src/server/config-serializer.ts
664
+ function serializeConfig(config) {
665
+ const registry = config.tools;
666
+ const webhookRegistry = config.webhooks;
667
+ return {
668
+ name: config.name,
669
+ version: config.version,
670
+ description: config.description,
671
+ computeLayer: config.computeLayer,
672
+ tools: registry ? Object.entries(registry).map(([key, tool]) => ({
673
+ name: tool.name || key,
674
+ description: tool.description,
675
+ timeout: tool.timeout,
676
+ retries: tool.retries
677
+ })) : [],
678
+ webhooks: webhookRegistry ? Object.values(webhookRegistry).map((w) => ({
679
+ name: w.name,
680
+ description: w.description,
681
+ methods: w.methods ?? ["POST"],
682
+ type: w.type ?? "WEBHOOK"
683
+ })) : [],
684
+ provision: isProvisionConfig(config.provision) ? config.provision : void 0,
685
+ agents: config.agents
686
+ };
687
+ }
688
+ function isProvisionConfig(value) {
689
+ return value !== void 0 && value !== null && !(value instanceof Promise);
690
+ }
691
+
692
+ // src/server/handlers/install-handler.ts
693
+ async function handleInstall(body, hooks) {
694
+ if (!hooks?.install) {
695
+ return {
696
+ status: 404,
697
+ body: { error: "Install handler not configured" }
698
+ };
715
699
  }
716
- if (value instanceof Promise) {
717
- const resolved = await value;
718
- if (resolved && typeof resolved === "object" && "default" in resolved) {
719
- return resolved.default;
700
+ if (!body.context?.appInstallationId || !body.context?.workplace) {
701
+ return {
702
+ status: 400,
703
+ body: {
704
+ error: {
705
+ code: -32602,
706
+ message: "Missing context (appInstallationId and workplace required)"
707
+ }
708
+ }
709
+ };
710
+ }
711
+ const installContext = {
712
+ env: body.env ?? {},
713
+ workplace: body.context.workplace,
714
+ appInstallationId: body.context.appInstallationId,
715
+ app: body.context.app,
716
+ invocation: body.invocation,
717
+ log: createContextLogger()
718
+ };
719
+ const requestConfig = {
720
+ baseUrl: body.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
721
+ apiToken: body.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
722
+ };
723
+ try {
724
+ const installHook = hooks.install;
725
+ const installHandler = typeof installHook === "function" ? installHook : installHook.handler;
726
+ const result = await runWithLogContext({ invocation: body.invocation }, async () => {
727
+ return await runWithConfig(requestConfig, async () => {
728
+ return await installHandler(installContext);
729
+ });
730
+ });
731
+ return {
732
+ status: 200,
733
+ body: { env: result.env ?? {}, redirect: result.redirect }
734
+ };
735
+ } catch (err) {
736
+ if (err instanceof InstallError) {
737
+ return {
738
+ status: 400,
739
+ body: {
740
+ error: {
741
+ code: err.code,
742
+ message: err.message,
743
+ field: err.field
744
+ }
745
+ }
746
+ };
720
747
  }
721
- return resolved;
748
+ return {
749
+ status: 500,
750
+ body: {
751
+ error: {
752
+ code: -32603,
753
+ message: err instanceof Error ? err.message : String(err ?? "")
754
+ }
755
+ }
756
+ };
722
757
  }
723
- return value;
724
758
  }
725
- function serializeTools(registry) {
726
- return Object.entries(registry).map(([key, tool]) => ({
727
- name: tool.name || key,
728
- displayName: tool.label,
729
- description: tool.description,
730
- timeout: tool.timeout,
731
- retries: tool.retries
732
- }));
759
+
760
+ // src/server/handlers/uninstall-handler.ts
761
+ async function handleUninstall(body, hooks) {
762
+ if (!hooks?.uninstall) {
763
+ return {
764
+ status: 404,
765
+ body: { error: "Uninstall handler not configured" }
766
+ };
767
+ }
768
+ if (!body.context?.appInstallationId || !body.context?.workplace || !body.context?.app) {
769
+ return {
770
+ status: 400,
771
+ body: {
772
+ error: {
773
+ code: -32602,
774
+ message: "Missing context (appInstallationId, workplace and app required)"
775
+ }
776
+ }
777
+ };
778
+ }
779
+ const uninstallContext = {
780
+ env: body.env ?? {},
781
+ workplace: body.context.workplace,
782
+ appInstallationId: body.context.appInstallationId,
783
+ app: body.context.app,
784
+ invocation: body.invocation,
785
+ log: createContextLogger()
786
+ };
787
+ const requestConfig = {
788
+ baseUrl: body.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
789
+ apiToken: body.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
790
+ };
791
+ try {
792
+ const uninstallHook = hooks.uninstall;
793
+ const uninstallHandlerFn = typeof uninstallHook === "function" ? uninstallHook : uninstallHook.handler;
794
+ const result = await runWithLogContext({ invocation: body.invocation }, async () => {
795
+ return await runWithConfig(requestConfig, async () => {
796
+ return await uninstallHandlerFn(uninstallContext);
797
+ });
798
+ });
799
+ return {
800
+ status: 200,
801
+ body: { cleanedWebhookIds: result.cleanedWebhookIds ?? [] }
802
+ };
803
+ } catch (err) {
804
+ return {
805
+ status: 500,
806
+ body: {
807
+ error: {
808
+ code: -32603,
809
+ message: err instanceof Error ? err.message : String(err ?? "")
810
+ }
811
+ }
812
+ };
813
+ }
733
814
  }
734
- function serializeWebhooks(registry) {
735
- return Object.values(registry).map((webhook) => ({
736
- name: webhook.name,
737
- description: webhook.description,
738
- methods: webhook.methods ?? ["POST"],
739
- type: webhook.type ?? "WEBHOOK"
740
- }));
815
+
816
+ // src/server/handlers/provision-handler.ts
817
+ async function handleProvision(body, hooks) {
818
+ if (!hooks?.provision) {
819
+ return {
820
+ status: 404,
821
+ body: { error: "Provision handler not configured" }
822
+ };
823
+ }
824
+ if (!body.context?.app) {
825
+ return {
826
+ status: 400,
827
+ body: {
828
+ error: {
829
+ code: -32602,
830
+ message: "Missing context (app required)"
831
+ }
832
+ }
833
+ };
834
+ }
835
+ const mergedEnv = {};
836
+ for (const [key, value] of Object.entries(process.env)) {
837
+ if (value !== void 0) {
838
+ mergedEnv[key] = value;
839
+ }
840
+ }
841
+ Object.assign(mergedEnv, body.env ?? {});
842
+ const provisionContext = {
843
+ env: mergedEnv,
844
+ app: body.context.app,
845
+ invocation: body.invocation,
846
+ log: createContextLogger()
847
+ };
848
+ const requestConfig = {
849
+ baseUrl: mergedEnv.SKEDYUL_API_URL ?? "",
850
+ apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? ""
851
+ };
852
+ try {
853
+ const provisionHook = hooks.provision;
854
+ const provisionHandler = typeof provisionHook === "function" ? provisionHook : provisionHook.handler;
855
+ const result = await runWithLogContext({ invocation: body.invocation }, async () => {
856
+ return await runWithConfig(requestConfig, async () => {
857
+ return await provisionHandler(provisionContext);
858
+ });
859
+ });
860
+ return {
861
+ status: 200,
862
+ body: result
863
+ };
864
+ } catch (err) {
865
+ return {
866
+ status: 500,
867
+ body: {
868
+ error: {
869
+ code: -32603,
870
+ message: err instanceof Error ? err.message : String(err ?? "")
871
+ }
872
+ }
873
+ };
874
+ }
741
875
  }
742
- async function resolveConfig(config, registry, webhookRegistry) {
743
- const provision = await resolveDynamicImport(
744
- config.provision
745
- );
746
- const install = await resolveDynamicImport(
747
- config.install
748
- );
749
- const tools = serializeTools(registry);
750
- const webhooks = webhookRegistry ? serializeWebhooks(webhookRegistry) : [];
876
+
877
+ // src/server/handler-helpers.ts
878
+ function parseHandlerEnvelope(parsedBody) {
879
+ if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody) || !("env" in parsedBody) || !("request" in parsedBody)) {
880
+ return null;
881
+ }
882
+ const envelope = parsedBody;
883
+ if (typeof envelope.env !== "object" || envelope.env === null || Array.isArray(envelope.env)) {
884
+ return null;
885
+ }
886
+ if (typeof envelope.request !== "object" || envelope.request === null || Array.isArray(envelope.request)) {
887
+ return null;
888
+ }
751
889
  return {
752
- name: config.name,
753
- version: config.version,
754
- description: config.description,
755
- computeLayer: config.computeLayer,
756
- tools,
757
- webhooks,
758
- provision,
759
- agents: config.agents
890
+ env: envelope.env,
891
+ request: envelope.request,
892
+ context: envelope.context
893
+ };
894
+ }
895
+ function buildRequestFromRaw(raw) {
896
+ let parsedBody = raw.body;
897
+ const contentType = raw.headers["content-type"] ?? "";
898
+ if (contentType.includes("application/json")) {
899
+ try {
900
+ parsedBody = raw.body ? JSON.parse(raw.body) : {};
901
+ } catch {
902
+ parsedBody = raw.body;
903
+ }
904
+ }
905
+ return {
906
+ method: raw.method,
907
+ url: raw.url,
908
+ path: raw.path,
909
+ headers: raw.headers,
910
+ query: raw.query,
911
+ body: parsedBody,
912
+ rawBody: raw.body ? Buffer.from(raw.body, "utf-8") : void 0
913
+ };
914
+ }
915
+ function buildRequestScopedConfig(env) {
916
+ return {
917
+ baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
918
+ apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
919
+ };
920
+ }
921
+
922
+ // src/server/handlers/oauth-callback-handler.ts
923
+ async function handleOAuthCallback(parsedBody, hooks) {
924
+ if (!hooks?.oauth_callback) {
925
+ return {
926
+ status: 404,
927
+ body: { error: "OAuth callback handler not configured" }
928
+ };
929
+ }
930
+ const envelope = parseHandlerEnvelope(parsedBody);
931
+ if (!envelope) {
932
+ console.error("[OAuth Callback] Failed to parse envelope. Body:", JSON.stringify(parsedBody, null, 2));
933
+ return {
934
+ status: 400,
935
+ body: {
936
+ error: {
937
+ code: -32602,
938
+ message: "Missing envelope format: expected { env, request }"
939
+ }
940
+ }
941
+ };
942
+ }
943
+ const invocation = parsedBody.invocation;
944
+ const oauthRequest = buildRequestFromRaw(envelope.request);
945
+ const requestConfig = buildRequestScopedConfig(envelope.env);
946
+ const oauthCallbackContext = {
947
+ request: oauthRequest,
948
+ invocation,
949
+ log: createContextLogger()
950
+ };
951
+ try {
952
+ const oauthCallbackHook = hooks.oauth_callback;
953
+ const oauthCallbackHandler = typeof oauthCallbackHook === "function" ? oauthCallbackHook : oauthCallbackHook.handler;
954
+ const result = await runWithLogContext({ invocation }, async () => {
955
+ return await runWithConfig(requestConfig, async () => {
956
+ return await oauthCallbackHandler(oauthCallbackContext);
957
+ });
958
+ });
959
+ return {
960
+ status: 200,
961
+ body: {
962
+ appInstallationId: result.appInstallationId,
963
+ env: result.env ?? {}
964
+ }
965
+ };
966
+ } catch (err) {
967
+ const errorMessage = err instanceof Error ? err.message : String(err ?? "Unknown error");
968
+ return {
969
+ status: 500,
970
+ body: {
971
+ error: {
972
+ code: -32603,
973
+ message: errorMessage
974
+ }
975
+ }
976
+ };
977
+ }
978
+ }
979
+
980
+ // src/server/handlers/webhook-handler.ts
981
+ function parseWebhookRequest(parsedBody, method, url, path, headers, query, rawBody, appIdHeader, appVersionIdHeader) {
982
+ const isEnvelope = typeof parsedBody === "object" && parsedBody !== null && "env" in parsedBody && "request" in parsedBody && "context" in parsedBody;
983
+ if (isEnvelope) {
984
+ const envelope = parsedBody;
985
+ const requestEnv = envelope.env ?? {};
986
+ const invocation = envelope.invocation;
987
+ let originalParsedBody = envelope.request.body;
988
+ const originalContentType = envelope.request.headers["content-type"] ?? "";
989
+ if (originalContentType.includes("application/json")) {
990
+ try {
991
+ originalParsedBody = envelope.request.body ? JSON.parse(envelope.request.body) : {};
992
+ } catch {
993
+ }
994
+ }
995
+ const webhookRequest2 = {
996
+ method: envelope.request.method,
997
+ url: envelope.request.url,
998
+ path: envelope.request.path,
999
+ headers: envelope.request.headers,
1000
+ query: envelope.request.query,
1001
+ body: originalParsedBody,
1002
+ rawBody: envelope.request.body ? Buffer.from(envelope.request.body, "utf-8") : void 0
1003
+ };
1004
+ const envVars = { ...process.env, ...requestEnv };
1005
+ const app = envelope.context.app;
1006
+ let webhookContext2;
1007
+ if (envelope.context.appInstallationId && envelope.context.workplace) {
1008
+ webhookContext2 = {
1009
+ env: envVars,
1010
+ app,
1011
+ appInstallationId: envelope.context.appInstallationId,
1012
+ workplace: envelope.context.workplace,
1013
+ registration: envelope.context.registration ?? {},
1014
+ invocation,
1015
+ log: createContextLogger()
1016
+ };
1017
+ } else {
1018
+ webhookContext2 = {
1019
+ env: envVars,
1020
+ app,
1021
+ invocation,
1022
+ log: createContextLogger()
1023
+ };
1024
+ }
1025
+ return { webhookRequest: webhookRequest2, webhookContext: webhookContext2, requestEnv, invocation };
1026
+ }
1027
+ if (!appIdHeader || !appVersionIdHeader) {
1028
+ return {
1029
+ error: "Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)"
1030
+ };
1031
+ }
1032
+ const webhookRequest = {
1033
+ method,
1034
+ url,
1035
+ path,
1036
+ headers,
1037
+ query,
1038
+ body: parsedBody,
1039
+ rawBody: rawBody ? Buffer.from(rawBody, "utf-8") : void 0
1040
+ };
1041
+ const webhookContext = {
1042
+ env: process.env,
1043
+ app: { id: appIdHeader, versionId: appVersionIdHeader },
1044
+ log: createContextLogger()
760
1045
  };
1046
+ return { webhookRequest, webhookContext, requestEnv: {} };
761
1047
  }
762
- function createMinimalConfig(name, version) {
1048
+ async function executeWebhookHandler(handle, webhookRegistry, data) {
1049
+ const webhookDef = webhookRegistry[handle];
1050
+ if (!webhookDef) {
1051
+ return {
1052
+ status: 404,
1053
+ body: { error: `Webhook handler '${handle}' not found` }
1054
+ };
1055
+ }
1056
+ const originalEnv = { ...process.env };
1057
+ Object.assign(process.env, data.requestEnv);
1058
+ const requestConfig = {
1059
+ baseUrl: data.requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
1060
+ apiToken: data.requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
1061
+ };
1062
+ let webhookResponse;
1063
+ try {
1064
+ webhookResponse = await runWithLogContext({ invocation: data.invocation }, async () => {
1065
+ return await runWithConfig(requestConfig, async () => {
1066
+ return await webhookDef.handler(data.webhookRequest, data.webhookContext);
1067
+ });
1068
+ });
1069
+ } catch (err) {
1070
+ console.error(`Webhook handler '${handle}' error:`, err);
1071
+ return {
1072
+ status: 500,
1073
+ body: { error: "Webhook handler error" }
1074
+ };
1075
+ } finally {
1076
+ process.env = originalEnv;
1077
+ }
763
1078
  return {
764
- name,
765
- version
1079
+ status: webhookResponse.status ?? 200,
1080
+ body: webhookResponse.body,
1081
+ headers: webhookResponse.headers
766
1082
  };
767
1083
  }
1084
+ function isMethodAllowed(webhookRegistry, handle, method) {
1085
+ const webhookDef = webhookRegistry[handle];
1086
+ if (!webhookDef) return false;
1087
+ const allowedMethods = webhookDef.methods ?? ["POST"];
1088
+ return allowedMethods.includes(method);
1089
+ }
768
1090
 
769
1091
  // src/server/dedicated.ts
770
- function createDedicatedServerInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry) {
1092
+ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer) {
771
1093
  const port = getListeningPort(config);
1094
+ const registry = config.tools;
1095
+ const webhookRegistry = config.webhooks;
772
1096
  const httpServer = http.createServer(
773
1097
  async (req, res) => {
774
1098
  function sendCoreResult(result) {
@@ -785,30 +1109,16 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
785
1109
  return;
786
1110
  }
787
1111
  if (pathname === "/config" && req.method === "GET") {
788
- let appConfig = config.appConfig;
789
- if (!appConfig && config.appConfigLoader) {
790
- const loaded = await config.appConfigLoader();
791
- appConfig = loaded.default;
792
- }
793
- if (!appConfig) {
794
- appConfig = createMinimalConfig(
795
- config.metadata.name,
796
- config.metadata.version
797
- );
798
- }
799
- const serializedConfig = await resolveConfig(appConfig, registry, webhookRegistry);
800
- sendJSON(res, 200, serializedConfig);
1112
+ sendJSON(res, 200, serializeConfig(config));
801
1113
  return;
802
1114
  }
803
1115
  if (pathname.startsWith("/webhooks/") && webhookRegistry) {
804
1116
  const handle = pathname.slice("/webhooks/".length);
805
- const webhookDef = webhookRegistry[handle];
806
- if (!webhookDef) {
1117
+ if (!webhookRegistry[handle]) {
807
1118
  sendJSON(res, 404, { error: `Webhook handler '${handle}' not found` });
808
1119
  return;
809
1120
  }
810
- const allowedMethods = webhookDef.methods ?? ["POST"];
811
- if (!allowedMethods.includes(req.method)) {
1121
+ if (!isMethodAllowed(webhookRegistry, handle, req.method ?? "POST")) {
812
1122
  sendJSON(res, 405, { error: `Method ${req.method} not allowed` });
813
1123
  return;
814
1124
  }
@@ -830,87 +1140,34 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
830
1140
  } else {
831
1141
  parsedBody = rawBody;
832
1142
  }
833
- const envelope = parseHandlerEnvelope(parsedBody);
834
- let webhookRequest;
835
- let webhookContext;
836
- let requestEnv = {};
837
- let invocation;
838
- if (envelope && "context" in envelope && envelope.context) {
839
- const context = envelope.context;
840
- requestEnv = envelope.env;
841
- invocation = parsedBody.invocation;
842
- webhookRequest = buildRequestFromRaw(envelope.request);
843
- const envVars = { ...process.env, ...envelope.env };
844
- const app = context.app;
845
- if (context.appInstallationId && context.workplace) {
846
- webhookContext = {
847
- env: envVars,
848
- app,
849
- appInstallationId: context.appInstallationId,
850
- workplace: context.workplace,
851
- registration: context.registration ?? {},
852
- invocation,
853
- log: createContextLogger()
854
- };
855
- } else {
856
- webhookContext = {
857
- env: envVars,
858
- app,
859
- invocation,
860
- log: createContextLogger()
861
- };
862
- }
863
- } else {
864
- const appId = req.headers["x-skedyul-app-id"];
865
- const appVersionId = req.headers["x-skedyul-app-version-id"];
866
- if (!appId || !appVersionId) {
867
- throw new Error("Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)");
868
- }
869
- webhookRequest = {
870
- method: req.method ?? "POST",
871
- url: url.toString(),
872
- path: pathname,
873
- headers: req.headers,
874
- query: Object.fromEntries(url.searchParams.entries()),
875
- body: parsedBody,
876
- rawBody: rawBody ? Buffer.from(rawBody, "utf-8") : void 0
877
- };
878
- webhookContext = {
879
- env: process.env,
880
- app: { id: appId, versionId: appVersionId },
881
- log: createContextLogger()
882
- };
883
- }
884
- const originalEnv = { ...process.env };
885
- Object.assign(process.env, requestEnv);
886
- const requestConfig = buildRequestScopedConfig(requestEnv);
887
- let webhookResponse;
888
- try {
889
- webhookResponse = await runWithLogContext({ invocation }, async () => {
890
- return await runWithConfig(requestConfig, async () => {
891
- return await webhookDef.handler(webhookRequest, webhookContext);
892
- });
893
- });
894
- } catch (err) {
895
- console.error(`Webhook handler '${handle}' error:`, err);
896
- sendJSON(res, 500, { error: "Webhook handler error" });
1143
+ const parseResult = parseWebhookRequest(
1144
+ parsedBody,
1145
+ req.method ?? "POST",
1146
+ url.toString(),
1147
+ pathname,
1148
+ req.headers,
1149
+ Object.fromEntries(url.searchParams.entries()),
1150
+ rawBody,
1151
+ req.headers["x-skedyul-app-id"],
1152
+ req.headers["x-skedyul-app-version-id"]
1153
+ );
1154
+ if ("error" in parseResult) {
1155
+ sendJSON(res, 400, { error: parseResult.error });
897
1156
  return;
898
- } finally {
899
- process.env = originalEnv;
900
1157
  }
901
- const status = webhookResponse.status ?? 200;
1158
+ const result = await executeWebhookHandler(handle, webhookRegistry, parseResult);
902
1159
  const responseHeaders = {
903
- ...webhookResponse.headers
1160
+ ...result.headers
904
1161
  };
905
1162
  if (!responseHeaders["Content-Type"] && !responseHeaders["content-type"]) {
906
1163
  responseHeaders["Content-Type"] = "application/json";
907
1164
  }
908
- res.writeHead(status, responseHeaders);
909
- if (webhookResponse.body !== void 0) {
910
- if (typeof webhookResponse.body === "string") {
911
- res.end(webhookResponse.body);
1165
+ res.writeHead(result.status, responseHeaders);
1166
+ if (result.body !== void 0) {
1167
+ if (typeof result.body === "string") {
1168
+ res.end(result.body);
912
1169
  } else {
913
- res.end(JSON.stringify(webhookResponse.body));
1170
+ res.end(JSON.stringify(result.body));
914
1171
  }
915
1172
  } else {
916
1173
  res.end();
@@ -949,10 +1206,6 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
949
1206
  return;
950
1207
  }
951
1208
  if (pathname === "/oauth_callback" && req.method === "POST") {
952
- if (!config.hooks?.oauth_callback) {
953
- sendJSON(res, 404, { error: "OAuth callback handler not configured" });
954
- return;
955
- }
956
1209
  let parsedBody;
957
1210
  try {
958
1211
  parsedBody = await parseJSONBody(req);
@@ -963,50 +1216,11 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
963
1216
  });
964
1217
  return;
965
1218
  }
966
- const envelope = parseHandlerEnvelope(parsedBody);
967
- if (!envelope) {
968
- console.error("[OAuth Callback] Failed to parse envelope. Body:", JSON.stringify(parsedBody, null, 2));
969
- sendJSON(res, 400, {
970
- error: { code: -32602, message: "Missing envelope format: expected { env, request }" }
971
- });
972
- return;
973
- }
974
- const invocation = parsedBody.invocation;
975
- const oauthRequest = buildRequestFromRaw(envelope.request);
976
- const oauthCallbackRequestConfig = buildRequestScopedConfig(envelope.env);
977
- const oauthCallbackContext = {
978
- request: oauthRequest,
979
- invocation,
980
- log: createContextLogger()
981
- };
982
- try {
983
- const oauthCallbackHook = config.hooks.oauth_callback;
984
- const oauthCallbackHandler = typeof oauthCallbackHook === "function" ? oauthCallbackHook : oauthCallbackHook.handler;
985
- const result = await runWithLogContext({ invocation }, async () => {
986
- return await runWithConfig(oauthCallbackRequestConfig, async () => {
987
- return await oauthCallbackHandler(oauthCallbackContext);
988
- });
989
- });
990
- sendJSON(res, 200, {
991
- appInstallationId: result.appInstallationId,
992
- env: result.env ?? {}
993
- });
994
- } catch (err) {
995
- const errorMessage = err instanceof Error ? err.message : String(err ?? "Unknown error");
996
- sendJSON(res, 500, {
997
- error: {
998
- code: -32603,
999
- message: errorMessage
1000
- }
1001
- });
1002
- }
1219
+ const result = await handleOAuthCallback(parsedBody, config.hooks);
1220
+ sendJSON(res, result.status, result.body);
1003
1221
  return;
1004
1222
  }
1005
1223
  if (pathname === "/install" && req.method === "POST") {
1006
- if (!config.hooks?.install) {
1007
- sendJSON(res, 404, { error: "Install handler not configured" });
1008
- return;
1009
- }
1010
1224
  let installBody;
1011
1225
  try {
1012
1226
  installBody = await parseJSONBody(req);
@@ -1016,61 +1230,11 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
1016
1230
  });
1017
1231
  return;
1018
1232
  }
1019
- if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
1020
- sendJSON(res, 400, {
1021
- error: { code: -32602, message: "Missing context (appInstallationId and workplace required)" }
1022
- });
1023
- return;
1024
- }
1025
- const installContext = {
1026
- env: installBody.env ?? {},
1027
- workplace: installBody.context.workplace,
1028
- appInstallationId: installBody.context.appInstallationId,
1029
- app: installBody.context.app,
1030
- invocation: installBody.invocation,
1031
- log: createContextLogger()
1032
- };
1033
- const installRequestConfig = {
1034
- baseUrl: installBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
1035
- apiToken: installBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
1036
- };
1037
- try {
1038
- const installHook = config.hooks.install;
1039
- const installHandler = typeof installHook === "function" ? installHook : installHook.handler;
1040
- const result = await runWithLogContext({ invocation: installBody.invocation }, async () => {
1041
- return await runWithConfig(installRequestConfig, async () => {
1042
- return await installHandler(installContext);
1043
- });
1044
- });
1045
- sendJSON(res, 200, {
1046
- env: result.env ?? {},
1047
- redirect: result.redirect
1048
- });
1049
- } catch (err) {
1050
- if (err instanceof InstallError) {
1051
- sendJSON(res, 400, {
1052
- error: {
1053
- code: err.code,
1054
- message: err.message,
1055
- field: err.field
1056
- }
1057
- });
1058
- } else {
1059
- sendJSON(res, 500, {
1060
- error: {
1061
- code: -32603,
1062
- message: err instanceof Error ? err.message : String(err ?? "")
1063
- }
1064
- });
1065
- }
1066
- }
1233
+ const result = await handleInstall(installBody, config.hooks);
1234
+ sendJSON(res, result.status, result.body);
1067
1235
  return;
1068
1236
  }
1069
1237
  if (pathname === "/uninstall" && req.method === "POST") {
1070
- if (!config.hooks?.uninstall) {
1071
- sendJSON(res, 404, { error: "Uninstall handler not configured" });
1072
- return;
1073
- }
1074
1238
  let uninstallBody;
1075
1239
  try {
1076
1240
  uninstallBody = await parseJSONBody(req);
@@ -1080,53 +1244,11 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
1080
1244
  });
1081
1245
  return;
1082
1246
  }
1083
- if (!uninstallBody.context?.appInstallationId || !uninstallBody.context?.workplace || !uninstallBody.context?.app) {
1084
- sendJSON(res, 400, {
1085
- error: {
1086
- code: -32602,
1087
- message: "Missing context (appInstallationId, workplace and app required)"
1088
- }
1089
- });
1090
- return;
1091
- }
1092
- const uninstallContext = {
1093
- env: uninstallBody.env ?? {},
1094
- workplace: uninstallBody.context.workplace,
1095
- appInstallationId: uninstallBody.context.appInstallationId,
1096
- app: uninstallBody.context.app,
1097
- invocation: uninstallBody.invocation,
1098
- log: createContextLogger()
1099
- };
1100
- const uninstallRequestConfig = {
1101
- baseUrl: uninstallBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
1102
- apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
1103
- };
1104
- try {
1105
- const uninstallHook = config.hooks.uninstall;
1106
- const uninstallHandlerFn = typeof uninstallHook === "function" ? uninstallHook : uninstallHook.handler;
1107
- const result = await runWithLogContext({ invocation: uninstallBody.invocation }, async () => {
1108
- return await runWithConfig(uninstallRequestConfig, async () => {
1109
- return await uninstallHandlerFn(uninstallContext);
1110
- });
1111
- });
1112
- sendJSON(res, 200, {
1113
- cleanedWebhookIds: result.cleanedWebhookIds ?? []
1114
- });
1115
- } catch (err) {
1116
- sendJSON(res, 500, {
1117
- error: {
1118
- code: -32603,
1119
- message: err instanceof Error ? err.message : String(err ?? "")
1120
- }
1121
- });
1122
- }
1247
+ const result = await handleUninstall(uninstallBody, config.hooks);
1248
+ sendJSON(res, result.status, result.body);
1123
1249
  return;
1124
1250
  }
1125
1251
  if (pathname === "/provision" && req.method === "POST") {
1126
- if (!config.hooks?.provision) {
1127
- sendJSON(res, 404, { error: "Provision handler not configured" });
1128
- return;
1129
- }
1130
1252
  let provisionBody;
1131
1253
  try {
1132
1254
  provisionBody = await parseJSONBody(req);
@@ -1136,46 +1258,8 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
1136
1258
  });
1137
1259
  return;
1138
1260
  }
1139
- if (!provisionBody.context?.app) {
1140
- sendJSON(res, 400, {
1141
- error: { code: -32602, message: "Missing context (app required)" }
1142
- });
1143
- return;
1144
- }
1145
- const mergedEnv = {};
1146
- for (const [key, value] of Object.entries(process.env)) {
1147
- if (value !== void 0) {
1148
- mergedEnv[key] = value;
1149
- }
1150
- }
1151
- Object.assign(mergedEnv, provisionBody.env ?? {});
1152
- const provisionContext = {
1153
- env: mergedEnv,
1154
- app: provisionBody.context.app,
1155
- invocation: provisionBody.invocation,
1156
- log: createContextLogger()
1157
- };
1158
- const provisionRequestConfig = {
1159
- baseUrl: mergedEnv.SKEDYUL_API_URL ?? "",
1160
- apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? ""
1161
- };
1162
- try {
1163
- const provisionHook = config.hooks.provision;
1164
- const provisionHandler = typeof provisionHook === "function" ? provisionHook : provisionHook.handler;
1165
- const result = await runWithLogContext({ invocation: provisionBody.invocation }, async () => {
1166
- return await runWithConfig(provisionRequestConfig, async () => {
1167
- return await provisionHandler(provisionContext);
1168
- });
1169
- });
1170
- sendJSON(res, 200, result);
1171
- } catch (err) {
1172
- sendJSON(res, 500, {
1173
- error: {
1174
- code: -32603,
1175
- message: err instanceof Error ? err.message : String(err ?? "")
1176
- }
1177
- });
1178
- }
1261
+ const result = await handleProvision(provisionBody, config.hooks);
1262
+ sendJSON(res, result.status, result.body);
1179
1263
  return;
1180
1264
  }
1181
1265
  if (pathname === "/core" && req.method === "POST") {
@@ -1326,7 +1410,7 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
1326
1410
  const finalPort = listenPort ?? port;
1327
1411
  return new Promise((resolve, reject) => {
1328
1412
  httpServer.listen(finalPort, () => {
1329
- printStartupLog(config, tools, webhookRegistry, finalPort);
1413
+ printStartupLog(config, tools, finalPort);
1330
1414
  resolve();
1331
1415
  });
1332
1416
  httpServer.once("error", reject);
@@ -1337,13 +1421,15 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
1337
1421
  }
1338
1422
 
1339
1423
  // src/server/serverless.ts
1340
- function createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry) {
1424
+ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
1341
1425
  const headers = getDefaultHeaders(config.cors);
1426
+ const registry = config.tools;
1427
+ const webhookRegistry = config.webhooks;
1342
1428
  let hasLoggedStartup = false;
1343
1429
  return {
1344
1430
  async handler(event) {
1345
1431
  if (!hasLoggedStartup) {
1346
- printStartupLog(config, tools, webhookRegistry);
1432
+ printStartupLog(config, tools);
1347
1433
  hasLoggedStartup = true;
1348
1434
  }
1349
1435
  try {
@@ -1354,12 +1440,10 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
1354
1440
  }
1355
1441
  if (path.startsWith("/webhooks/") && webhookRegistry) {
1356
1442
  const handle = path.slice("/webhooks/".length);
1357
- const webhookDef = webhookRegistry[handle];
1358
- if (!webhookDef) {
1443
+ if (!webhookRegistry[handle]) {
1359
1444
  return createResponse(404, { error: `Webhook handler '${handle}' not found` }, headers);
1360
1445
  }
1361
- const allowedMethods = webhookDef.methods ?? ["POST"];
1362
- if (!allowedMethods.includes(method)) {
1446
+ if (!isMethodAllowed(webhookRegistry, handle, method)) {
1363
1447
  return createResponse(405, { error: `Method ${method} not allowed` }, headers);
1364
1448
  }
1365
1449
  const rawBody = event.body ?? "";
@@ -1374,107 +1458,34 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
1374
1458
  } else {
1375
1459
  parsedBody = rawBody;
1376
1460
  }
1377
- const isEnvelope = typeof parsedBody === "object" && parsedBody !== null && "env" in parsedBody && "request" in parsedBody && "context" in parsedBody;
1378
- let webhookRequest;
1379
- let webhookContext;
1380
- let requestEnv = {};
1381
- let invocation;
1382
- if (isEnvelope) {
1383
- const envelope = parsedBody;
1384
- requestEnv = envelope.env ?? {};
1385
- invocation = envelope.invocation;
1386
- let originalParsedBody = envelope.request.body;
1387
- const originalContentType = envelope.request.headers["content-type"] ?? "";
1388
- if (originalContentType.includes("application/json")) {
1389
- try {
1390
- originalParsedBody = envelope.request.body ? JSON.parse(envelope.request.body) : {};
1391
- } catch {
1392
- }
1393
- }
1394
- webhookRequest = {
1395
- method: envelope.request.method,
1396
- url: envelope.request.url,
1397
- path: envelope.request.path,
1398
- headers: envelope.request.headers,
1399
- query: envelope.request.query,
1400
- body: originalParsedBody,
1401
- rawBody: envelope.request.body ? Buffer.from(envelope.request.body, "utf-8") : void 0
1402
- };
1403
- const envVars = { ...process.env, ...requestEnv };
1404
- const app = envelope.context.app;
1405
- if (envelope.context.appInstallationId && envelope.context.workplace) {
1406
- webhookContext = {
1407
- env: envVars,
1408
- app,
1409
- appInstallationId: envelope.context.appInstallationId,
1410
- workplace: envelope.context.workplace,
1411
- registration: envelope.context.registration ?? {},
1412
- invocation,
1413
- log: createContextLogger()
1414
- };
1415
- } else {
1416
- webhookContext = {
1417
- env: envVars,
1418
- app,
1419
- invocation,
1420
- log: createContextLogger()
1421
- };
1422
- }
1423
- } else {
1424
- const appId = event.headers?.["x-skedyul-app-id"] ?? event.headers?.["X-Skedyul-App-Id"];
1425
- const appVersionId = event.headers?.["x-skedyul-app-version-id"] ?? event.headers?.["X-Skedyul-App-Version-Id"];
1426
- if (!appId || !appVersionId) {
1427
- throw new Error("Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)");
1428
- }
1429
- const forwardedProto = event.headers?.["x-forwarded-proto"] ?? event.headers?.["X-Forwarded-Proto"];
1430
- const protocol = forwardedProto ?? "https";
1431
- const host = event.headers?.host ?? event.headers?.Host ?? "localhost";
1432
- const queryString = event.queryStringParameters ? "?" + new URLSearchParams(event.queryStringParameters).toString() : "";
1433
- const webhookUrl = `${protocol}://${host}${path}${queryString}`;
1434
- webhookRequest = {
1435
- method,
1436
- url: webhookUrl,
1437
- path,
1438
- headers: event.headers,
1439
- query: event.queryStringParameters ?? {},
1440
- body: parsedBody,
1441
- rawBody: rawBody ? Buffer.from(rawBody, "utf-8") : void 0
1442
- };
1443
- webhookContext = {
1444
- env: process.env,
1445
- app: { id: appId, versionId: appVersionId },
1446
- log: createContextLogger()
1447
- };
1448
- }
1449
- const originalEnv = { ...process.env };
1450
- Object.assign(process.env, requestEnv);
1451
- const requestConfig = {
1452
- baseUrl: requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
1453
- apiToken: requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
1454
- };
1455
- let webhookResponse;
1456
- try {
1457
- webhookResponse = await runWithLogContext({ invocation }, async () => {
1458
- return await runWithConfig(requestConfig, async () => {
1459
- return await webhookDef.handler(webhookRequest, webhookContext);
1460
- });
1461
- });
1462
- } catch (err) {
1463
- console.error(`Webhook handler '${handle}' error:`, err);
1464
- return createResponse(500, { error: "Webhook handler error" }, headers);
1465
- } finally {
1466
- process.env = originalEnv;
1461
+ const forwardedProto = event.headers?.["x-forwarded-proto"] ?? event.headers?.["X-Forwarded-Proto"];
1462
+ const protocol = forwardedProto ?? "https";
1463
+ const host = event.headers?.host ?? event.headers?.Host ?? "localhost";
1464
+ const queryString = event.queryStringParameters ? "?" + new URLSearchParams(event.queryStringParameters).toString() : "";
1465
+ const webhookUrl = `${protocol}://${host}${path}${queryString}`;
1466
+ const parseResult = parseWebhookRequest(
1467
+ parsedBody,
1468
+ method,
1469
+ webhookUrl,
1470
+ path,
1471
+ event.headers,
1472
+ event.queryStringParameters ?? {},
1473
+ rawBody,
1474
+ event.headers?.["x-skedyul-app-id"] ?? event.headers?.["X-Skedyul-App-Id"],
1475
+ event.headers?.["x-skedyul-app-version-id"] ?? event.headers?.["X-Skedyul-App-Version-Id"]
1476
+ );
1477
+ if ("error" in parseResult) {
1478
+ return createResponse(400, { error: parseResult.error }, headers);
1467
1479
  }
1480
+ const result = await executeWebhookHandler(handle, webhookRegistry, parseResult);
1468
1481
  const responseHeaders = {
1469
1482
  ...headers,
1470
- ...webhookResponse.headers
1483
+ ...result.headers
1471
1484
  };
1472
- const status = webhookResponse.status ?? 200;
1473
- const body = webhookResponse.body;
1474
1485
  return {
1475
- statusCode: status,
1486
+ statusCode: result.status,
1476
1487
  headers: responseHeaders,
1477
- body: body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : ""
1488
+ body: result.body !== void 0 ? typeof result.body === "string" ? result.body : JSON.stringify(result.body) : ""
1478
1489
  };
1479
1490
  }
1480
1491
  if (path === "/core" && method === "POST") {
@@ -1610,9 +1621,6 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
1610
1621
  }
1611
1622
  }
1612
1623
  if (path === "/install" && method === "POST") {
1613
- if (!config.hooks?.install) {
1614
- return createResponse(404, { error: "Install handler not configured" }, headers);
1615
- }
1616
1624
  let installBody;
1617
1625
  try {
1618
1626
  installBody = event.body ? JSON.parse(event.body) : {};
@@ -1623,68 +1631,10 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
1623
1631
  headers
1624
1632
  );
1625
1633
  }
1626
- if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
1627
- return createResponse(
1628
- 400,
1629
- { error: { code: -32602, message: "Missing context (appInstallationId and workplace required)" } },
1630
- headers
1631
- );
1632
- }
1633
- const installContext = {
1634
- env: installBody.env ?? {},
1635
- workplace: installBody.context.workplace,
1636
- appInstallationId: installBody.context.appInstallationId,
1637
- app: installBody.context.app,
1638
- invocation: installBody.invocation,
1639
- log: createContextLogger()
1640
- };
1641
- const installRequestConfig = {
1642
- baseUrl: installBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
1643
- apiToken: installBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
1644
- };
1645
- try {
1646
- const installHook = config.hooks.install;
1647
- const installHandler = typeof installHook === "function" ? installHook : installHook.handler;
1648
- const result = await runWithLogContext({ invocation: installBody.invocation }, async () => {
1649
- return await runWithConfig(installRequestConfig, async () => {
1650
- return await installHandler(installContext);
1651
- });
1652
- });
1653
- return createResponse(
1654
- 200,
1655
- { env: result.env ?? {}, redirect: result.redirect },
1656
- headers
1657
- );
1658
- } catch (err) {
1659
- if (err instanceof InstallError) {
1660
- return createResponse(
1661
- 400,
1662
- {
1663
- error: {
1664
- code: err.code,
1665
- message: err.message,
1666
- field: err.field
1667
- }
1668
- },
1669
- headers
1670
- );
1671
- }
1672
- return createResponse(
1673
- 500,
1674
- {
1675
- error: {
1676
- code: -32603,
1677
- message: err instanceof Error ? err.message : String(err ?? "")
1678
- }
1679
- },
1680
- headers
1681
- );
1682
- }
1634
+ const result = await handleInstall(installBody, config.hooks);
1635
+ return createResponse(result.status, result.body, headers);
1683
1636
  }
1684
1637
  if (path === "/uninstall" && method === "POST") {
1685
- if (!config.hooks?.uninstall) {
1686
- return createResponse(404, { error: "Uninstall handler not configured" }, headers);
1687
- }
1688
1638
  let uninstallBody;
1689
1639
  try {
1690
1640
  uninstallBody = event.body ? JSON.parse(event.body) : {};
@@ -1695,137 +1645,24 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
1695
1645
  headers
1696
1646
  );
1697
1647
  }
1698
- if (!uninstallBody.context?.appInstallationId || !uninstallBody.context?.workplace || !uninstallBody.context?.app) {
1699
- return createResponse(
1700
- 400,
1701
- {
1702
- error: {
1703
- code: -32602,
1704
- message: "Missing context (appInstallationId, workplace and app required)"
1705
- }
1706
- },
1707
- headers
1708
- );
1709
- }
1710
- const uninstallContext = {
1711
- env: uninstallBody.env ?? {},
1712
- workplace: uninstallBody.context.workplace,
1713
- appInstallationId: uninstallBody.context.appInstallationId,
1714
- app: uninstallBody.context.app,
1715
- invocation: uninstallBody.invocation,
1716
- log: createContextLogger()
1717
- };
1718
- const uninstallRequestConfig = {
1719
- baseUrl: uninstallBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
1720
- apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
1721
- };
1722
- try {
1723
- const uninstallHook = config.hooks.uninstall;
1724
- const uninstallHandlerFn = typeof uninstallHook === "function" ? uninstallHook : uninstallHook.handler;
1725
- const result = await runWithLogContext({ invocation: uninstallBody.invocation }, async () => {
1726
- return await runWithConfig(uninstallRequestConfig, async () => {
1727
- return await uninstallHandlerFn(uninstallContext);
1728
- });
1729
- });
1730
- return createResponse(
1731
- 200,
1732
- { cleanedWebhookIds: result.cleanedWebhookIds ?? [] },
1733
- headers
1734
- );
1735
- } catch (err) {
1736
- return createResponse(
1737
- 500,
1738
- {
1739
- error: {
1740
- code: -32603,
1741
- message: err instanceof Error ? err.message : String(err ?? "")
1742
- }
1743
- },
1744
- headers
1745
- );
1746
- }
1648
+ const result = await handleUninstall(uninstallBody, config.hooks);
1649
+ return createResponse(result.status, result.body, headers);
1747
1650
  }
1748
1651
  if (path === "/provision" && method === "POST") {
1749
- console.log("[serverless] /provision endpoint called");
1750
- if (!config.hooks?.provision) {
1751
- console.log("[serverless] No provision handler configured");
1752
- return createResponse(404, { error: "Provision handler not configured" }, headers);
1753
- }
1754
1652
  let provisionBody;
1755
1653
  try {
1756
1654
  provisionBody = event.body ? JSON.parse(event.body) : {};
1757
- console.log("[serverless] Provision body parsed:", {
1758
- hasEnv: !!provisionBody.env,
1759
- hasContext: !!provisionBody.context,
1760
- appId: provisionBody.context?.app?.id,
1761
- versionId: provisionBody.context?.app?.versionId
1762
- });
1763
1655
  } catch {
1764
- console.log("[serverless] Failed to parse provision body");
1765
1656
  return createResponse(
1766
1657
  400,
1767
1658
  { error: { code: -32700, message: "Parse error" } },
1768
1659
  headers
1769
1660
  );
1770
1661
  }
1771
- if (!provisionBody.context?.app) {
1772
- console.log("[serverless] Missing app context in provision body");
1773
- return createResponse(
1774
- 400,
1775
- { error: { code: -32602, message: "Missing context (app required)" } },
1776
- headers
1777
- );
1778
- }
1779
- const mergedEnv = {};
1780
- for (const [key, value] of Object.entries(process.env)) {
1781
- if (value !== void 0) {
1782
- mergedEnv[key] = value;
1783
- }
1784
- }
1785
- Object.assign(mergedEnv, provisionBody.env ?? {});
1786
- const provisionContext = {
1787
- env: mergedEnv,
1788
- app: provisionBody.context.app,
1789
- invocation: provisionBody.invocation,
1790
- log: createContextLogger()
1791
- };
1792
- const provisionRequestConfig = {
1793
- baseUrl: mergedEnv.SKEDYUL_API_URL ?? "",
1794
- apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? ""
1795
- };
1796
- console.log("[serverless] Calling provision handler...");
1797
- try {
1798
- const provisionHook = config.hooks.provision;
1799
- const provisionHandler = typeof provisionHook === "function" ? provisionHook : provisionHook.handler;
1800
- const result = await runWithLogContext({ invocation: provisionBody.invocation }, async () => {
1801
- return await runWithConfig(provisionRequestConfig, async () => {
1802
- return await provisionHandler(provisionContext);
1803
- });
1804
- });
1805
- console.log("[serverless] Provision handler completed successfully");
1806
- return createResponse(200, result, headers);
1807
- } catch (err) {
1808
- console.error("[serverless] Provision handler failed:", err instanceof Error ? err.message : String(err));
1809
- return createResponse(
1810
- 500,
1811
- {
1812
- error: {
1813
- code: -32603,
1814
- message: err instanceof Error ? err.message : String(err ?? "")
1815
- }
1816
- },
1817
- headers
1818
- );
1819
- }
1662
+ const result = await handleProvision(provisionBody, config.hooks);
1663
+ return createResponse(result.status, result.body, headers);
1820
1664
  }
1821
1665
  if (path === "/oauth_callback" && method === "POST") {
1822
- if (!config.hooks?.oauth_callback) {
1823
- return createResponse(
1824
- 404,
1825
- { error: "OAuth callback handler not configured" },
1826
- headers
1827
- );
1828
- }
1829
1666
  let parsedBody;
1830
1667
  try {
1831
1668
  parsedBody = event.body ? JSON.parse(event.body) : {};
@@ -1837,70 +1674,14 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
1837
1674
  headers
1838
1675
  );
1839
1676
  }
1840
- const envelope = parseHandlerEnvelope(parsedBody);
1841
- if (!envelope) {
1842
- console.error("[OAuth Callback] Failed to parse envelope. Body:", JSON.stringify(parsedBody, null, 2));
1843
- return createResponse(
1844
- 400,
1845
- { error: { code: -32602, message: "Missing envelope format: expected { env, request }" } },
1846
- headers
1847
- );
1848
- }
1849
- const invocation = parsedBody.invocation;
1850
- const oauthRequest = buildRequestFromRaw(envelope.request);
1851
- const oauthCallbackRequestConfig = buildRequestScopedConfig(envelope.env);
1852
- const oauthCallbackContext = {
1853
- request: oauthRequest,
1854
- invocation,
1855
- log: createContextLogger()
1856
- };
1857
- try {
1858
- const oauthCallbackHook = config.hooks.oauth_callback;
1859
- const oauthCallbackHandler = typeof oauthCallbackHook === "function" ? oauthCallbackHook : oauthCallbackHook.handler;
1860
- const result = await runWithLogContext({ invocation }, async () => {
1861
- return await runWithConfig(oauthCallbackRequestConfig, async () => {
1862
- return await oauthCallbackHandler(oauthCallbackContext);
1863
- });
1864
- });
1865
- return createResponse(
1866
- 200,
1867
- {
1868
- appInstallationId: result.appInstallationId,
1869
- env: result.env ?? {}
1870
- },
1871
- headers
1872
- );
1873
- } catch (err) {
1874
- const errorMessage = err instanceof Error ? err.message : String(err ?? "Unknown error");
1875
- return createResponse(
1876
- 500,
1877
- {
1878
- error: {
1879
- code: -32603,
1880
- message: errorMessage
1881
- }
1882
- },
1883
- headers
1884
- );
1885
- }
1677
+ const result = await handleOAuthCallback(parsedBody, config.hooks);
1678
+ return createResponse(result.status, result.body, headers);
1886
1679
  }
1887
1680
  if (path === "/health" && method === "GET") {
1888
1681
  return createResponse(200, state.getHealthStatus(), headers);
1889
1682
  }
1890
1683
  if (path === "/config" && method === "GET") {
1891
- let appConfig = config.appConfig;
1892
- if (!appConfig && config.appConfigLoader) {
1893
- const loaded = await config.appConfigLoader();
1894
- appConfig = loaded.default;
1895
- }
1896
- if (!appConfig) {
1897
- appConfig = createMinimalConfig(
1898
- config.metadata.name,
1899
- config.metadata.version
1900
- );
1901
- }
1902
- const serializedConfig = await resolveConfig(appConfig, registry, webhookRegistry);
1903
- return createResponse(200, serializedConfig, headers);
1684
+ return createResponse(200, serializeConfig(config), headers);
1904
1685
  }
1905
1686
  if (path === "/mcp" && method === "POST") {
1906
1687
  let body;
@@ -2118,9 +1899,11 @@ console.log("[skedyul-node/server] All imports complete");
2118
1899
  console.log("[skedyul-node/server] Installing context logger...");
2119
1900
  installContextLogger();
2120
1901
  console.log("[skedyul-node/server] Context logger installed");
2121
- function createSkedyulServer(config, registry, webhookRegistry) {
1902
+ function createSkedyulServer(config) {
2122
1903
  console.log("[createSkedyulServer] Step 1: mergeRuntimeEnv()");
2123
1904
  mergeRuntimeEnv();
1905
+ const registry = config.tools;
1906
+ const webhookRegistry = config.webhooks;
2124
1907
  console.log("[createSkedyulServer] Step 2: coreApi setup");
2125
1908
  if (config.coreApi?.service) {
2126
1909
  coreApiService.register(config.coreApi.service);
@@ -2132,7 +1915,7 @@ function createSkedyulServer(config, registry, webhookRegistry) {
2132
1915
  const tools = buildToolMetadata(registry);
2133
1916
  console.log("[createSkedyulServer] Step 3 done, tools:", tools.length);
2134
1917
  const toolNames = Object.values(registry).map((tool) => tool.name);
2135
- const runtimeLabel = config.computeLayer;
1918
+ const runtimeLabel = config.computeLayer ?? "serverless";
2136
1919
  const maxRequests = config.maxRequests ?? parseNumberEnv(process.env.MCP_MAX_REQUESTS) ?? null;
2137
1920
  const ttlExtendSeconds = config.ttlExtendSeconds ?? parseNumberEnv(process.env.MCP_TTL_EXTEND) ?? 3600;
2138
1921
  console.log("[createSkedyulServer] Step 4: createRequestState()");
@@ -2145,8 +1928,8 @@ function createSkedyulServer(config, registry, webhookRegistry) {
2145
1928
  console.log("[createSkedyulServer] Step 4 done");
2146
1929
  console.log("[createSkedyulServer] Step 5: new McpServer()");
2147
1930
  const mcpServer = new McpServer({
2148
- name: config.metadata.name,
2149
- version: config.metadata.version
1931
+ name: config.name,
1932
+ version: config.version ?? "0.0.0"
2150
1933
  });
2151
1934
  console.log("[createSkedyulServer] Step 5 done");
2152
1935
  const dedicatedShutdown = () => {
@@ -2274,13 +2057,11 @@ function createSkedyulServer(config, registry, webhookRegistry) {
2274
2057
  tools,
2275
2058
  callTool,
2276
2059
  state,
2277
- mcpServer,
2278
- registry,
2279
- webhookRegistry
2060
+ mcpServer
2280
2061
  );
2281
2062
  }
2282
2063
  console.log("[createSkedyulServer] Creating serverless instance");
2283
- const serverlessInstance = createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry);
2064
+ const serverlessInstance = createServerlessInstance(config, tools, callTool, state, mcpServer);
2284
2065
  console.log("[createSkedyulServer] Serverless instance created successfully");
2285
2066
  return serverlessInstance;
2286
2067
  }
@@ -2288,32 +2069,10 @@ var server = {
2288
2069
  create: createSkedyulServer
2289
2070
  };
2290
2071
  export {
2291
- buildRequestFromRaw,
2292
- buildRequestScopedConfig,
2293
- buildToolMetadata,
2294
- createCallToolHandler,
2295
- createDedicatedServerInstance,
2296
- createRequestState,
2297
- createResponse,
2298
- createServerlessInstance,
2299
2072
  createSkedyulServer,
2300
- getDefaultHeaders,
2301
2073
  getJsonSchemaFromToolSchema,
2302
- getListeningPort,
2303
2074
  getZodSchema,
2304
- handleCoreMethod,
2305
2075
  isToolSchemaWithJson,
2306
- mergeRuntimeEnv,
2307
- normalizeBilling,
2308
- padEnd,
2309
- parseHandlerEnvelope,
2310
- parseJSONBody,
2311
- parseJsonRecord,
2312
- parseNumberEnv,
2313
- printStartupLog,
2314
- readRawRequestBody,
2315
- sendHTML,
2316
- sendJSON,
2317
2076
  server,
2318
2077
  toJsonSchema
2319
2078
  };