@setluca/mcp 0.1.1 → 0.2.1

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 (63) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +178 -80
  3. package/dist/annotations.js +45 -0
  4. package/dist/completions.js +110 -0
  5. package/dist/config.js +55 -29
  6. package/dist/errors.js +56 -15
  7. package/dist/generated/openapi-inputs.js +4324 -1429
  8. package/dist/generated/openapi-outputs.js +21992 -8516
  9. package/dist/generated/route-catalog.js +1480 -0
  10. package/dist/http.js +97 -103
  11. package/dist/index.js +16 -10
  12. package/dist/oauth-resolver.js +59 -37
  13. package/dist/observability.js +12 -4
  14. package/dist/openapi-zod.js +138 -72
  15. package/dist/operations/catalog.js +50 -0
  16. package/dist/operations/groups/analytics.js +122 -0
  17. package/dist/operations/groups/bookings.js +189 -0
  18. package/dist/operations/groups/broadcasts.js +106 -0
  19. package/dist/operations/groups/cadences.js +14 -0
  20. package/dist/operations/groups/callEvents.js +131 -0
  21. package/dist/operations/groups/campaigns.js +132 -0
  22. package/dist/operations/groups/capabilities.js +8 -0
  23. package/dist/operations/groups/coach.js +43 -0
  24. package/dist/operations/groups/conversations.js +48 -0
  25. package/dist/operations/groups/insights.js +33 -0
  26. package/dist/operations/groups/integrations.js +80 -0
  27. package/dist/operations/groups/knowledge.js +34 -0
  28. package/dist/operations/groups/leads.js +101 -0
  29. package/dist/operations/groups/learning.js +24 -0
  30. package/dist/operations/groups/memory.js +27 -0
  31. package/dist/operations/groups/reports.js +14 -0
  32. package/dist/operations/groups/reviewQueue.js +57 -0
  33. package/dist/operations/groups/safety.js +8 -0
  34. package/dist/operations/groups/usage.js +13 -0
  35. package/dist/operations/groups/voice.js +14 -0
  36. package/dist/operations/groups/webhooks.js +76 -0
  37. package/dist/operations/manifest.js +40 -0
  38. package/dist/operations/registry.js +331 -0
  39. package/dist/operations.js +11 -780
  40. package/dist/optional-field.js +12 -0
  41. package/dist/page-shape.js +62 -0
  42. package/dist/pagination.js +165 -0
  43. package/dist/prompts.js +89 -8
  44. package/dist/provenance.js +18 -2
  45. package/dist/remote.js +80 -49
  46. package/dist/resilience.js +127 -0
  47. package/dist/resources.js +17 -2
  48. package/dist/scopes.js +154 -0
  49. package/dist/serialization.js +36 -0
  50. package/dist/server.js +258 -316
  51. package/dist/task-tools/analytics.js +168 -0
  52. package/dist/task-tools/bookings.js +157 -0
  53. package/dist/task-tools/cadences.js +65 -0
  54. package/dist/task-tools/calls.js +202 -0
  55. package/dist/task-tools/inbox.js +120 -0
  56. package/dist/task-tools/leads.js +123 -0
  57. package/dist/task-tools/runtime.js +92 -0
  58. package/dist/task-tools.js +21 -0
  59. package/dist/tool-cache.js +39 -0
  60. package/dist/tool.js +228 -0
  61. package/dist/tsconfig.tsbuildinfo +1 -1
  62. package/dist/version.js +14 -0
  63. package/package.json +38 -34
package/dist/config.js CHANGED
@@ -1,47 +1,73 @@
1
- import { Context, Effect, Layer } from "effect";
1
+ import { Context, Effect, Layer, Match, Option } from "effect";
2
+ import * as z from "zod/v4";
2
3
  import { LucaConfigError } from "./errors.js";
4
+ import { optionalField } from "./optional-field.js";
3
5
  const TRAILING_SLASHES = /\/+$/;
6
+ /** How long one HTTP attempt may run before it is cancelled and retried. */
7
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
4
8
  export const LucaConfig = Context.GenericTag("@luca/mcp/LucaConfig");
5
9
  function nonEmpty(value) {
6
- const trimmed = value?.trim();
7
- return trimmed ? trimmed : undefined;
10
+ // An all-whitespace variable is the same as an unset one, so it collapses to
11
+ // undefined and lets every caller's `??` fallback take over.
12
+ return value?.trim() || undefined;
8
13
  }
9
14
  function normalizeBaseUrl(value) {
10
15
  const raw = nonEmpty(value) ?? "https://api.setluca.com";
11
16
  return raw.replace(TRAILING_SLASHES, "");
12
17
  }
13
- function parseAuthHeader(value) {
14
- const normalized = nonEmpty(value)?.toLowerCase();
15
- if (!normalized || normalized === "x-api-key") {
16
- return "x-api-key";
17
- }
18
- if (normalized === "authorization" || normalized === "bearer") {
19
- return "authorization";
20
- }
21
- throw new LucaConfigError({
22
- message: "LUCA_AUTH_HEADER must be x-api-key or authorization",
23
- });
18
+ /**
19
+ * Every spelling of LUCA_AUTH_HEADER we accept, mapped to the header the client
20
+ * actually sends. "bearer" is here because it is what people write when they
21
+ * mean the Authorization header, and refusing it would be a papercut.
22
+ */
23
+ const AUTH_HEADER_ALIASES = new Map([
24
+ ["x-api-key", "x-api-key"],
25
+ ["authorization", "authorization"],
26
+ ["bearer", "authorization"],
27
+ ]);
28
+ /**
29
+ * The per-attempt timeout a variable asks for, absent when it is unset or is
30
+ * not a positive whole number of milliseconds. Absent means the caller's
31
+ * default applies, so a typo degrades to the default rather than to no timeout
32
+ * at all.
33
+ */
34
+ export function parseTimeoutMs(value) {
35
+ const parsed = z.coerce
36
+ .number()
37
+ .int()
38
+ .positive()
39
+ .safeParse(nonEmpty(value) ?? undefined);
40
+ return parsed.success ? parsed.data : undefined;
24
41
  }
25
- export function loadConfig(env = process.env) {
26
- const apiKey = nonEmpty(env.LUCA_API_KEY) ?? nonEmpty(env.LUCA_API_TOKEN);
27
- if (!apiKey) {
42
+ function parseAuthHeader(value) {
43
+ const alias = nonEmpty(value)?.toLowerCase() ?? "x-api-key";
44
+ return Match.value(AUTH_HEADER_ALIASES.get(alias)).pipe(Match.when(Match.undefined, () => {
28
45
  throw new LucaConfigError({
29
- message: "Set LUCA_API_KEY to a Luca developer API key.",
46
+ message: "LUCA_AUTH_HEADER must be x-api-key or authorization",
30
47
  });
31
- }
32
- const workspaceId = nonEmpty(env.LUCA_WORKSPACE_ID);
33
- const workspaceSlug = nonEmpty(env.LUCA_WORKSPACE_SLUG);
48
+ }), Match.orElse((header) => header));
49
+ }
50
+ export function loadConfig(env = process.env) {
51
+ const apiKey = Option.fromNullable(nonEmpty(env.LUCA_API_KEY) ?? nonEmpty(env.LUCA_API_TOKEN)).pipe(Option.getOrThrowWith(() => new LucaConfigError({
52
+ message: "Set LUCA_API_KEY to a Luca developer API key.",
53
+ })));
34
54
  return {
35
55
  apiBaseUrl: normalizeBaseUrl(env.LUCA_API_BASE_URL),
36
56
  apiKey,
37
57
  authHeader: parseAuthHeader(env.LUCA_AUTH_HEADER),
38
- ...(workspaceId ? { workspaceId } : {}),
39
- ...(workspaceSlug ? { workspaceSlug } : {}),
58
+ ...optionalField("workspaceId", nonEmpty(env.LUCA_WORKSPACE_ID)),
59
+ ...optionalField("workspaceSlug", nonEmpty(env.LUCA_WORKSPACE_SLUG)),
60
+ ...optionalField("requestTimeoutMs", parseTimeoutMs(env.LUCA_REQUEST_TIMEOUT_MS)),
40
61
  };
41
62
  }
42
- export const LucaConfigLive = Layer.effect(LucaConfig, Effect.try({
43
- try: () => loadConfig(),
44
- catch: (cause) => cause instanceof LucaConfigError
45
- ? cause
46
- : new LucaConfigError({ message: String(cause) }),
47
- }));
63
+ /**
64
+ * Every way `loadConfig` can fail has to reach the caller as a `LucaConfigError`
65
+ * so the layer's error channel stays one type. A config error passes through
66
+ * untouched. Anything else is wrapped rather than escaping as an untyped
67
+ * defect, whether that is a getter on the environment that throws or a future
68
+ * caller of this helper raising something unexpected.
69
+ */
70
+ export function toConfigError(cause) {
71
+ return Match.value(cause).pipe(Match.when(Match.instanceOf(LucaConfigError), (error) => error), Match.orElse((other) => new LucaConfigError({ message: String(other) })));
72
+ }
73
+ export const LucaConfigLive = Layer.effect(LucaConfig, Effect.try({ try: () => loadConfig(), catch: toConfigError }));
package/dist/errors.js CHANGED
@@ -1,4 +1,6 @@
1
- import { Data } from "effect";
1
+ import { Data, Match, Option } from "effect";
2
+ import * as z from "zod/v4";
3
+ import { toJsonPayload } from "./serialization.js";
2
4
  export class LucaConfigError extends Data.TaggedError("LucaConfigError") {
3
5
  }
4
6
  export class LucaNetworkError extends Data.TaggedError("LucaNetworkError") {
@@ -7,19 +9,58 @@ export class LucaDecodeError extends Data.TaggedError("LucaDecodeError") {
7
9
  }
8
10
  export class LucaHttpError extends Data.TaggedError("LucaHttpError") {
9
11
  }
12
+ /**
13
+ * One attempt ran past the request timeout and was cancelled. It is its own
14
+ * error rather than a network error because a caller can retry it safely under
15
+ * the same rules but cannot tell from a network failure whether the request
16
+ * reached the API.
17
+ */
18
+ export class LucaTimeoutError extends Data.TaggedError("LucaTimeoutError") {
19
+ }
20
+ /**
21
+ * A tool was called with arguments that are not a JSON object. The MCP SDK
22
+ * validates against the tool's own input schema before the handler runs, so a
23
+ * real client cannot reach this. It exists so no code ahead of `runPromise` can
24
+ * throw where the error channel cannot see it.
25
+ */
26
+ export class LucaToolInputError extends Data.TaggedError("LucaToolInputError") {
27
+ }
28
+ const DECODE_BODY_EXCERPT = 500;
29
+ function formatDecodeError(error) {
30
+ return `${error.message}: ${error.body.slice(0, DECODE_BODY_EXCERPT)}`;
31
+ }
32
+ function formatHttpError(error) {
33
+ const request = Option.fromNullable(error.requestId).pipe(Option.match({
34
+ onNone: () => "",
35
+ onSome: (requestId) => ` requestId=${requestId}`,
36
+ }));
37
+ return `Luca API returned ${error.status} ${error.statusText}${request}: ${toJsonPayload(error.body)}`;
38
+ }
39
+ /**
40
+ * A value that carries no message of its own, rendered for a human. A string is
41
+ * handed back as it was thrown, since quoting it would put the reader a step
42
+ * further from the value than the throw site was. Everything else comes back as
43
+ * JSON rather than "[object Object]". Numbers and booleans need no case of
44
+ * their own: JSON renders each of them exactly as `String` would.
45
+ */
46
+ function formatJsonValue(error) {
47
+ const text = z.string().safeParse(error);
48
+ return text.success ? text.data : toJsonPayload(error);
49
+ }
50
+ /** Any failure this server can hand a client, as one line of text. */
10
51
  export function formatError(error) {
11
- if (error instanceof LucaConfigError) {
12
- return error.message;
13
- }
14
- if (error instanceof LucaNetworkError) {
15
- return error.message;
16
- }
17
- if (error instanceof LucaDecodeError) {
18
- return `${error.message}: ${error.body.slice(0, 500)}`;
19
- }
20
- if (error instanceof LucaHttpError) {
21
- const request = error.requestId ? ` requestId=${error.requestId}` : "";
22
- return `Luca API returned ${error.status} ${error.statusText}${request}: ${JSON.stringify(error.body)}`;
23
- }
24
- return error instanceof Error ? error.message : String(error);
52
+ return error instanceof Error ? formatThrown(error) : formatJsonValue(error);
53
+ }
54
+ /**
55
+ * A thrown value rendered for a human. The two structured errors get their own
56
+ * rendering; every other tagged error carries a `message`, and one branch
57
+ * covers all of them rather than a case per tag that would all say the same
58
+ * thing.
59
+ *
60
+ * The match is on the class, not on `_tag`. This takes any `Error`, including
61
+ * one thrown by a dependency that carries no tag at all, so there is no tagged
62
+ * union here for `Match.tag` to narrow.
63
+ */
64
+ function formatThrown(error) {
65
+ return Match.value(error).pipe(Match.when(Match.instanceOf(LucaDecodeError), formatDecodeError), Match.when(Match.instanceOf(LucaHttpError), formatHttpError), Match.orElse((thrown) => thrown.message));
25
66
  }