@setluca/mcp 0.2.0 → 0.3.0

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 (72) hide show
  1. package/CHANGELOG.md +426 -0
  2. package/LICENSE +21 -0
  3. package/README.md +19 -258
  4. package/dist/annotations.js +35 -20
  5. package/dist/client-address.js +64 -0
  6. package/dist/completions.js +44 -27
  7. package/dist/config.js +89 -38
  8. package/dist/errors.js +99 -22
  9. package/dist/fields.js +68 -0
  10. package/dist/fixed-window.js +46 -0
  11. package/dist/generated/openapi-inputs.js +216 -5820
  12. package/dist/generated/openapi-outputs.js +19014 -14001
  13. package/dist/generated/route-catalog.js +80 -0
  14. package/dist/http.js +108 -44
  15. package/dist/index.js +4 -1
  16. package/dist/oauth-challenge.js +74 -0
  17. package/dist/oauth-resolver.js +58 -61
  18. package/dist/observability.js +31 -16
  19. package/dist/openapi-schema.js +175 -0
  20. package/dist/operations/catalog.js +17 -0
  21. package/dist/operations/groups/analytics.js +20 -10
  22. package/dist/operations/groups/bookings.js +43 -15
  23. package/dist/operations/groups/broadcasts.js +29 -58
  24. package/dist/operations/groups/cadences.js +3 -1
  25. package/dist/operations/groups/callEvents.js +19 -72
  26. package/dist/operations/groups/campaigns.js +28 -32
  27. package/dist/operations/groups/channels.js +23 -0
  28. package/dist/operations/groups/conversations.js +14 -25
  29. package/dist/operations/groups/insights.js +4 -0
  30. package/dist/operations/groups/integrations.js +15 -17
  31. package/dist/operations/groups/knowledge.js +5 -1
  32. package/dist/operations/groups/leads.js +22 -23
  33. package/dist/operations/groups/learning.js +13 -1
  34. package/dist/operations/groups/memory.js +4 -6
  35. package/dist/operations/groups/reports.js +1 -1
  36. package/dist/operations/groups/reviewQueue.js +17 -14
  37. package/dist/operations/groups/safety.js +1 -1
  38. package/dist/operations/groups/voice.js +0 -1
  39. package/dist/operations/groups/webhooks.js +16 -4
  40. package/dist/operations/manifest.js +9 -8
  41. package/dist/operations/registry.js +110 -116
  42. package/dist/operations.js +6 -5
  43. package/dist/optional-field.js +15 -3
  44. package/dist/page-contract.js +74 -0
  45. package/dist/pagination.js +149 -68
  46. package/dist/prompts.js +139 -88
  47. package/dist/provenance.js +2 -2
  48. package/dist/rate-limit.js +40 -0
  49. package/dist/remote.js +145 -65
  50. package/dist/resilience.js +120 -55
  51. package/dist/resolvers.js +125 -0
  52. package/dist/resources.js +17 -2
  53. package/dist/scopes.js +207 -0
  54. package/dist/serialization.js +32 -22
  55. package/dist/server.js +174 -191
  56. package/dist/standard-schema.js +215 -0
  57. package/dist/task-tools/analytics.js +145 -86
  58. package/dist/task-tools/bookings.js +176 -86
  59. package/dist/task-tools/cadences.js +30 -45
  60. package/dist/task-tools/calls.js +132 -125
  61. package/dist/task-tools/inbox.js +89 -53
  62. package/dist/task-tools/leads.js +49 -77
  63. package/dist/task-tools/runtime.js +80 -20
  64. package/dist/task-tools.js +1 -1
  65. package/dist/token-verification.js +42 -0
  66. package/dist/tool-cache.js +3 -2
  67. package/dist/tool.js +155 -100
  68. package/dist/version.js +1 -1
  69. package/package.json +40 -18
  70. package/dist/openapi-zod.js +0 -333
  71. package/dist/page-shape.js +0 -47
  72. package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/tool.js CHANGED
@@ -1,13 +1,24 @@
1
- import { Duration, Effect, Layer } from "effect";
2
- import * as z from "zod/v4";
3
- import { mutatesExisting, toolAnnotations } from "./annotations.js";
4
- import { formatError, LucaToolInputError } from "./errors.js";
1
+ import * as Cause from "effect/Cause";
2
+ import * as Duration from "effect/Duration";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Exit from "effect/Exit";
5
+ import * as Option from "effect/Option";
6
+ import * as Schema from "effect/Schema";
7
+ import { operationAnnotationSource, toolAnnotations } from "./annotations.js";
8
+ import { formatError, LucaConfirmationRequired, LucaToolInputError, } from "./errors.js";
9
+ import { confirmInput } from "./fields.js";
5
10
  import { LucaApi } from "./http.js";
11
+ import { authChallengeMeta } from "./oauth-challenge.js";
6
12
  import { logAgentSurfaceEvent } from "./observability.js";
13
+ import { openApiToolOutputFields } from "./openapi-schema.js";
14
+ import { needsConfirmation, operationById, } from "./operations.js";
7
15
  import { optionalField } from "./optional-field.js";
8
16
  import { paginatedProgram } from "./pagination.js";
9
17
  import { provenanceFields, UNTRUSTED_CONTENT_NOTE } from "./provenance.js";
10
- import { JsonObjectSchema, toJsonText, } from "./serialization.js";
18
+ import { oauthScopeForOperation, oauthScopeForTool, } from "./scopes.js";
19
+ import { JsonObject, toJsonText } from "./serialization.js";
20
+ import { toolInputSchema, toolSchema } from "./standard-schema.js";
21
+ import { taskRequest } from "./task-tools/runtime.js";
11
22
  import { buildCache } from "./tool-cache.js";
12
23
  /**
13
24
  * Each derivation below is cached on the identity of the catalog entry it comes
@@ -17,75 +28,99 @@ import { buildCache } from "./tool-cache.js";
17
28
  */
18
29
  const operationTools = buildCache();
19
30
  const taskTools = buildCache();
20
- const objectSchemas = buildCache();
21
- /** A tool's fields as one Zod object, built once per fields object. */
22
- function objectSchema(fields) {
23
- return objectSchemas.get(fields, () => z.object(fields));
31
+ const inputSchemas = buildCache();
32
+ const outputSchemas = buildCache();
33
+ /**
34
+ * A tool's fields as one Standard Schema object, built once per fields object.
35
+ * Building one renders its JSON Schema, which is the expensive part. Inputs and
36
+ * outputs are cached apart because only an input refuses undeclared keys.
37
+ */
38
+ function inputObjectSchema(fields) {
39
+ return inputSchemas.get(fields, () => toolInputSchema(fields));
40
+ }
41
+ function outputObjectSchema(fields) {
42
+ return outputSchemas.get(fields, () => toolSchema(fields));
24
43
  }
25
44
  export function fromOperation(operation) {
26
45
  return operationTools.get(operation, () => buildFromOperation(operation));
27
46
  }
28
47
  function buildFromOperation(operation) {
29
48
  return {
49
+ ...operationAnnotationSource(operation),
30
50
  name: operation.toolName,
31
- title: operation.title,
32
51
  description: operationDescription(operation),
33
52
  inputSchema: operation.inputSchema,
34
53
  outputSchema: operation.outputSchema,
35
- readOnly: operation.method === "GET",
36
- mutatesExisting: mutatesExisting(operation.method),
37
- confirmRequired: operation.confirmRequired,
38
- idempotencyRequired: operation.idempotencyRequired,
39
54
  untrustedContent: operation.untrustedContent,
55
+ oauthScope: oauthScopeForOperation(operation),
40
56
  run: (input) => operationProgram(operation, input),
41
57
  };
42
58
  }
59
+ export function fromTaskTool(tool) {
60
+ return taskTools.get(tool, () => buildFromTaskTool(tool));
61
+ }
62
+ function confirmFor(tool, composed) {
63
+ if (composed.some((operation) => operation.confirm === "always")) {
64
+ return "always";
65
+ }
66
+ const conditional = composed.some((operation) => operation.confirm !== undefined);
67
+ return conditional ? (tool.confirm ?? "always") : tool.confirm;
68
+ }
43
69
  /**
44
- * `operationById` arrives as a parameter rather than an import: the lookup map
45
- * is built beside the server, and importing it here would close a cycle
46
- * between this module and the one that registers with it.
70
+ * Every safety flag on a task tool comes from the operations it composes, so a
71
+ * tool cannot claim to be safer than the routes it calls.
47
72
  */
48
- export function fromTaskTool(tool, operationById) {
49
- return taskTools.get(tool, () => buildFromTaskTool(tool, operationById));
50
- }
51
- function buildFromTaskTool(tool, operationById) {
73
+ function buildFromTaskTool(tool) {
74
+ const composed = tool.composes.map(operationById);
75
+ const untrustedContent = tool.untrustedContent === true ||
76
+ composed.some((operation) => operation.untrustedContent);
77
+ // One approval covers the whole call. A composed write gated for every call
78
+ // gates the task tool every time. A composed write gated only for some inputs
79
+ // cannot be evaluated here, because the tool's own input is not the input
80
+ // that write receives. The tool then has to name its own narrower gate, and
81
+ // without one it is gated every time.
82
+ const confirm = confirmFor(tool, composed);
52
83
  return {
53
84
  name: tool.name,
54
85
  title: tool.title,
55
86
  description: tool.description,
56
- inputSchema: tool.inputSchema,
57
- outputSchema: undefined,
58
- readOnly: tool.readOnly,
59
- // No task tool maps onto a single mutating route. A destructive one is
60
- // destructive because it reaches a real lead, which `confirmRequired`
61
- // already states.
62
- mutatesExisting: false,
63
- confirmRequired: tool.confirmRequired === true,
64
- // A task tool sends several requests, so no single key describes the call.
65
- // Each constituent write carries its own, minted by the HTTP client.
66
- idempotencyRequired: false,
67
- untrustedContent: tool.untrustedContent,
68
- run: (input) => Effect.gen(function* () {
69
- const api = yield* LucaApi;
70
- return yield* tool.run(api, operationById, input);
71
- }),
87
+ inputSchema: confirm === undefined
88
+ ? tool.inputSchema
89
+ : { ...tool.inputSchema, ...confirmInput },
90
+ outputSchema: {
91
+ ...openApiToolOutputFields({ body: null, untrustedContent }),
92
+ result: tool.outputSchema,
93
+ },
94
+ readOnly: composed.every((operation) => operation.method === "GET"),
95
+ // A task tool overwrites or removes existing state when any write it
96
+ // composes does.
97
+ mutatesExisting: composed.some((operation) => operation.mutatesExisting),
98
+ ...optionalField("confirm", confirm),
99
+ untrustedContent,
100
+ // A task tool reaches as far as the furthest operation it composes.
101
+ openWorld: composed.some((operation) => operation.openWorld),
102
+ oauthScope: oauthScopeForTool(composed),
103
+ run: (input) => tool.run((id, parts) => Effect.flatMap(LucaApi, (api) => api.request(taskRequest(operationById(id), input, parts))), input),
72
104
  };
73
105
  }
74
- /**
75
- * `toolset` arrives as a parameter rather than a field on the tool: the same
76
- * `LucaOperation` serves both surfaces, so a tool has no business claiming to
77
- * know which one registered it.
78
- */
79
- export function registerTool(server, tool, lucaLayer, toolset) {
106
+ export function registerTool(server, tool, lucaLayer, surface) {
80
107
  server.registerTool(tool.name, {
81
108
  title: tool.title,
82
109
  description: tool.description,
83
- inputSchema: objectSchema(tool.inputSchema),
84
- ...optionalField("outputSchema", tool.outputSchema === undefined
85
- ? undefined
86
- : objectSchema(tool.outputSchema)),
110
+ inputSchema: inputObjectSchema(tool.inputSchema),
111
+ outputSchema: outputObjectSchema(tool.outputSchema),
87
112
  annotations: toolAnnotations(tool),
88
- }, (input) => Effect.runPromise(toolResult(tool, lucaLayer, toolset, input)));
113
+ // ChatGPT reads this to know a tool needs a linked account, and which
114
+ // scope to ask for when it does.
115
+ _meta: {
116
+ securitySchemes: [{ type: "oauth2", scopes: [tool.oauthScope] }],
117
+ },
118
+ },
119
+ // The client's cancellation interrupts the program, which aborts the
120
+ // request it has in flight.
121
+ (input, ctx) => Effect.runPromise(toolResult(tool, lucaLayer, surface, input), {
122
+ signal: ctx.mcpReq.signal,
123
+ }));
89
124
  }
90
125
  /**
91
126
  * The call's arguments as a JSON object, failing into the error channel rather
@@ -93,59 +128,76 @@ export function registerTool(server, tool, lucaLayer, toolset) {
93
128
  * the handler runs, so a real client never lands here; the point is that no
94
129
  * code ahead of `runPromise` can throw where the error channel cannot see it.
95
130
  */
131
+ const decodeToolInput = Schema.decodeUnknownEffect(JsonObject);
132
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- the SDK hands over the arguments its own schema decoded, typed loosely; this is where they become a JSON object.
96
133
  function parseToolInput(input) {
97
- return Effect.try({
98
- try: () => JsonObjectSchema.parse(input),
99
- catch: () => new LucaToolInputError({
100
- message: "Tool arguments must be a JSON object",
101
- }),
102
- });
103
- }
104
- function ended(outcome, result) {
105
- return { outcome, result };
134
+ return decodeToolInput(input).pipe(Effect.mapError(() => new LucaToolInputError({
135
+ message: "Tool arguments must be a JSON object",
136
+ })));
106
137
  }
107
138
  /**
108
- * A tool call's result, confirmation gate included. Every way the call can end —
109
- * unreadable arguments, a rejected confirmation, a failed request, a thrown
110
- * defect while the request was being built — leaves as a tool result, so
111
- * nothing reaches the transport as an unhandled rejection.
112
- *
113
- * The log line sits after the defect handler, so a call produces exactly one
114
- * line whichever way it ended.
139
+ * A tool call's result, confirmation gate included. Every way the call can end
140
+ * leaves as a tool result, so nothing reaches the transport as an unhandled
141
+ * rejection, and every way logs exactly one line.
115
142
  */
116
- function toolResult(tool, lucaLayer, toolset, input) {
117
- return parseToolInput(input).pipe(Effect.flatMap((parsed) => gatedResult(tool, lucaLayer, parsed)), Effect.catchAll((error) => Effect.succeed(ended("error", asErrorResult(error)))), Effect.catchAllDefect((defect) => Effect.succeed(ended("defect", asErrorResult(defect instanceof Error ? defect : new Error(String(defect)))))), Effect.timed, Effect.tap(([elapsed, ended_]) => Effect.sync(() => {
118
- // The defect's own message never lands here: it can carry arbitrary
119
- // thrown text derived from the call's input. The client still sees it
120
- // in the tool result.
121
- logAgentSurfaceEvent("mcp.tool.called", {
122
- toolName: tool.name,
123
- toolset,
124
- outcome: ended_.outcome,
125
- durationMs: Math.round(Duration.toMillis(elapsed)),
126
- });
127
- })), Effect.map(([, ended_]) => ended_.result));
128
- }
129
- /** One tool call past the confirmation gate, or the gate's own refusal. */
130
- function gatedResult(tool, lucaLayer, input) {
131
- if (tool.confirmRequired && input.confirm !== true) {
132
- return Effect.succeed(ended("error", confirmationError(tool.name)));
133
- }
134
- return runTool(tool, lucaLayer, input);
143
+ function toolResult(tool, lucaLayer, surface,
144
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- input is the same raw MCP JSON-RPC tool-call arguments registerTool's server.registerTool callback hands in; parseToolInput below is what actually validates it.
145
+ input) {
146
+ const logCall = (outcome, elapsed) =>
147
+ // A defect's own message never lands here: it can carry arbitrary thrown
148
+ // text derived from the call's input. The client still sees it in the
149
+ // tool result.
150
+ logAgentSurfaceEvent("mcp.tool.called", {
151
+ toolName: tool.name,
152
+ toolset: surface.toolset,
153
+ outcome,
154
+ durationMs: elapsed === undefined
155
+ ? undefined
156
+ : Math.round(Duration.toMillis(elapsed)),
157
+ });
158
+ return parseToolInput(input).pipe(Effect.flatMap((parsed) => needsConfirmation(tool.confirm, parsed) && parsed.confirm !== true
159
+ ? Effect.fail(new LucaConfirmationRequired({ toolName: tool.name }))
160
+ : tool.run(parsed)), Effect.provide(lucaLayer), Effect.exit, Effect.timed, Effect.map(([elapsed, exit]) => {
161
+ const [outcome, result] = settle(tool, surface, exit);
162
+ logCall(outcome, elapsed);
163
+ return result;
164
+ }),
165
+ // A cancellation from the client stops the program before the exit above
166
+ // is read, so it is logged on the way out instead.
167
+ Effect.onInterrupt(() => Effect.sync(() => logCall("interrupted"))));
135
168
  }
136
- /** One confirmed tool call, run to a success or an error tool result. */
137
- function runTool(tool, lucaLayer, input) {
138
- return tool.run(input).pipe(Effect.provide(lucaLayer), Effect.match({
139
- onFailure: (error) => ended("error", asErrorResult(error)),
140
- onSuccess: (result) => ended("success", asSuccessResult(tool, result)),
141
- }));
169
+ /** A finished call as the outcome it logs and the result the client gets. */
170
+ function settle(tool, surface, exit) {
171
+ if (Exit.isSuccess(exit)) {
172
+ return ["success", asSuccessResult(tool, exit.value)];
173
+ }
174
+ const { cause } = exit;
175
+ const error = Cause.findErrorOption(cause);
176
+ if (Option.isSome(error)) {
177
+ const failure = error.value;
178
+ if (failure instanceof LucaConfirmationRequired) {
179
+ logAgentSurfaceEvent("mcp.write_gate.rejected", { toolName: tool.name });
180
+ return ["error", confirmationError(tool.name)];
181
+ }
182
+ return [
183
+ "error",
184
+ asErrorResult(failure, authChallengeMeta(failure, tool.oauthScope, surface.resourceMetadataUrl)),
185
+ ];
186
+ }
187
+ const defect = Cause.squash(cause);
188
+ return [
189
+ "defect",
190
+ asErrorResult(
191
+ // oxlint-disable-next-line effect/avoid-untagged-errors -- boundary narrows an unknown thrown value that carries no tag
192
+ defect instanceof Error ? defect : new Error(String(defect))),
193
+ ];
142
194
  }
143
195
  /** The Effect program that runs an operation, auto-paginating list tools. */
144
196
  export function operationProgram(operation, toolInput) {
145
197
  const baseRequest = operation.buildRequest(toolInput);
146
- const { pageShape } = operation;
147
- if (pageShape) {
148
- return paginatedProgram(operation, pageShape, baseRequest, toolInput.maxPages);
198
+ const { pageContract } = operation;
199
+ if (pageContract) {
200
+ return paginatedProgram(operation, pageContract, baseRequest, toolInput.maxPages);
149
201
  }
150
202
  // An operation with no page shape has no pagination to follow, so it is one
151
203
  // request, sent as-is.
@@ -156,8 +208,8 @@ export function operationProgram(operation, toolInput) {
156
208
  }
157
209
  /**
158
210
  * A tool's successful result. A response that can carry external-user text is
159
- * flagged untrusted on both halves — the structured `provenance.untrusted`
160
- * field (spoof-resistant) and a human-facing preamble on the text — so a
211
+ * flagged untrusted on both halves, the structured `provenance.untrusted`
212
+ * field that a lead cannot spoof and a human-facing preamble on the text, so a
161
213
  * downstream agent treats embedded text as data, not instructions. What the
162
214
  * structured half attaches is `provenanceFields`, shared with the resource
163
215
  * reads in `server.ts`.
@@ -179,7 +231,7 @@ function asSuccessResult(tool, result) {
179
231
  },
180
232
  };
181
233
  }
182
- function asErrorResult(error) {
234
+ function asErrorResult(error, meta) {
183
235
  return {
184
236
  isError: true,
185
237
  content: [
@@ -188,6 +240,7 @@ function asErrorResult(error) {
188
240
  text: formatError(error),
189
241
  },
190
242
  ],
243
+ ...optionalField("_meta", meta),
191
244
  };
192
245
  }
193
246
  function operationDescription(operation) {
@@ -196,25 +249,27 @@ function operationDescription(operation) {
196
249
  "",
197
250
  `Luca operation: ${operation.method} ${operation.path}`,
198
251
  `Required scopes: ${operation.scopes.join(", ")}`,
199
- `Required capability: ${operation.requiredScope.capability}; data sensitivity: ${operation.requiredScope.dataSensitivity}`,
252
+ `Required OAuth scope: ${oauthScopeForOperation(operation)}; data sensitivity: ${operation.requiredScope.dataSensitivity}`,
200
253
  operation.idempotencyRequired
201
254
  ? "Requires Idempotency-Key. Provide idempotencyKey for safe retries, or the server will generate one."
202
255
  : "Does not require idempotency.",
203
256
  ];
204
- if (operation.confirmRequired) {
205
- lines.push("Destructive: has a real-world side effect. You must pass confirm: true; the call is rejected without it.");
257
+ if (operation.confirm !== undefined) {
258
+ const when = operation.confirm === "always"
259
+ ? ""
260
+ : ` when ${operation.confirm.description}`;
261
+ lines.push(`Destructive${when}: has a real-world side effect. You must pass confirm: true; the call is rejected without it.`);
206
262
  }
207
263
  lines.push("Use workspaceId or workspaceSlug to override the default workspace configured by environment.");
208
264
  return lines.join("\n");
209
265
  }
210
266
  function confirmationError(toolName) {
211
- logAgentSurfaceEvent("mcp.write_gate.rejected", { toolName });
212
267
  return {
213
268
  isError: true,
214
269
  content: [
215
270
  {
216
271
  type: "text",
217
- text: `${toolName}: This tool requires confirm: true because it has a real-world side effect (sends messages to leads or re-triggers downstream automations). Re-call the tool with confirm: true to proceed.`,
272
+ text: `${toolName}: This tool requires confirm: true because it has a real-world side effect. Re-call the tool with confirm: true to proceed.`,
218
273
  },
219
274
  ],
220
275
  structuredContent: {
package/dist/version.js CHANGED
@@ -9,6 +9,6 @@
9
9
  *
10
10
  * Bump this in the same commit as package.json and server.json.
11
11
  */
12
- export const LUCA_MCP_VERSION = "0.2.0";
12
+ export const LUCA_MCP_VERSION = "0.3.0";
13
13
  /** The user-agent every outbound Luca API call identifies itself with. */
14
14
  export const LUCA_MCP_USER_AGENT = `luca-mcp/${LUCA_MCP_VERSION}`;
package/package.json CHANGED
@@ -1,22 +1,26 @@
1
1
  {
2
2
  "name": "@setluca/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
- "description": "Model Context Protocol server for the Luca public API.",
5
+ "description": "Let your AI assistant help with Luca conversations, leads, replies, campaigns, and bookings.",
6
6
  "keywords": [
7
7
  "ai",
8
- "crm",
9
8
  "luca",
10
9
  "mcp",
11
10
  "model-context-protocol"
12
11
  ],
13
12
  "license": "Apache-2.0",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/setluca/luca-mcp.git"
16
+ },
14
17
  "bin": {
15
18
  "luca-mcp": "dist/index.js"
16
19
  },
17
20
  "files": [
18
21
  "dist",
19
- "README.md"
22
+ "README.md",
23
+ "CHANGELOG.md"
20
24
  ],
21
25
  "type": "module",
22
26
  "exports": {
@@ -34,45 +38,63 @@
34
38
  "test": "TZ=UTC vitest run --exclude test/stdio-smoke.test.ts",
35
39
  "test:mutation": "TZ=UTC stryker run",
36
40
  "test:stdio": "vitest run test/stdio-smoke.test.ts",
41
+ "lint": "oxlint . --deny-warnings",
42
+ "lint:anti-slop": "oxlint . --deny-warnings",
43
+ "format": "oxfmt --write . --disable-nested-config",
44
+ "format:check": "oxfmt --check . --disable-nested-config",
45
+ "knip": "knip",
46
+ "fallow:dead-code": "fallow dead-code --fail-on-issues",
47
+ "fallow:dupes": "fallow dupes --fail-on-issues --summary",
48
+ "fallow:security": "fallow security --fail-on-issues --summary",
49
+ "fallow:health:gate": "fallow health --min-score 85 --summary",
50
+ "quality": "bun run format:check && bun run lint && bun run knip && bun run fallow:dead-code && bun run fallow:dupes && bun run fallow:security && bun run fallow:health:gate",
37
51
  "dev:remote": "wrangler dev",
38
52
  "deploy": "wrangler deploy --env production",
53
+ "deploy:dry-run": "wrangler deploy --env production --dry-run",
39
54
  "test:integration": "vitest run test/integration",
40
55
  "coverage": "bun run test -- --coverage",
41
56
  "typecheck": "tsc --noEmit",
42
- "verify": "bun run docs:check && bun run catalog:check && bun run openapi:inputs:check && bun run openapi:outputs:check && bun run openapi:check && bun run registry:check && bun run redaction:check && bun run typecheck && bun run test && bun run build && bun run test:stdio",
57
+ "verify": "bun run quality && bun run docs:check && bun run catalog:check && bun run openapi:inputs:check && bun run openapi:outputs:check && bun run openapi:check && bun run registry:check && bun run typecheck && bun run coverage && bun run build && bun run test:stdio && bun run contracts:deployed:check",
43
58
  "openapi:generate": "bun run catalog:generate && bun scripts/generate-openapi-inputs.ts --write && bun scripts/generate-openapi-outputs.ts --write",
59
+ "contracts:sync": "bun scripts/contract-sync.ts",
60
+ "contracts:deployed:check": "bun scripts/check-deployed-luca-contracts.ts",
44
61
  "catalog:generate": "bun scripts/generate-route-catalog.ts --write",
45
62
  "catalog:check": "bun scripts/generate-route-catalog.ts --check",
46
63
  "openapi:inputs:check": "bun scripts/generate-openapi-inputs.ts --check",
47
64
  "openapi:outputs:check": "bun scripts/generate-openapi-outputs.ts --check",
48
65
  "openapi:check": "bun scripts/check-openapi.ts",
49
66
  "registry:check": "bun scripts/check-registry.ts",
50
- "redaction:check": "bun scripts/check-redaction.ts",
51
67
  "docs:generate": "bun scripts/generate-docs.ts --write",
52
68
  "docs:check": "bun scripts/generate-docs.ts --check",
53
- "run-all": "bun run openapi:generate && bun run docs:generate && bun run typecheck && bun run test",
54
69
  "clean": "rm -rf .turbo dist coverage node_modules"
55
70
  },
56
71
  "dependencies": {
57
72
  "@modelcontextprotocol/server": "^2.0.0",
58
- "effect": "^3.22.1",
59
- "zod": "^4.4.3"
73
+ "effect": "4.0.0"
60
74
  },
61
75
  "devDependencies": {
62
- "@luca/config": "workspace:*",
63
- "@luca/test-utils": "workspace:*",
76
+ "@effect/platform-bun": "4.0.0",
77
+ "@effect/vitest": "4.0.0",
64
78
  "@modelcontextprotocol/client": "^2.0.0",
65
- "@modelcontextprotocol/inspector": "^2.2.0",
66
- "@types/node": "^26.2.0",
67
- "@vitest/coverage-v8": "^4.1.10",
68
- "oxfmt": "^0.63.0",
79
+ "@modelcontextprotocol/inspector": "^2.5.0",
80
+ "@mpsuesser/oxlint-plugin-effect": "0.6.0",
81
+ "@oxlint/plugins": "1.82.0",
82
+ "@stryker-mutator/core": "10.0.0",
83
+ "@stryker-mutator/vitest-runner": "10.0.0",
84
+ "@types/node": "^26.5.0",
85
+ "@vitest/coverage-v8": "^5.0.0",
86
+ "fallow": "3.23.0",
87
+ "knip": "6.34.0",
88
+ "oxfmt": "^0.67.0",
89
+ "oxlint": "^1.82.0",
69
90
  "typescript": "^7.0.2",
70
- "vitest": "^4.1.10",
71
- "wrangler": "^4.123.0"
91
+ "ultracite": "7.11.0",
92
+ "vitest": "^5.0.0",
93
+ "wrangler": "4.113.0"
72
94
  },
73
95
  "engines": {
74
96
  "node": ">=20"
75
97
  },
76
- "//publish": "Publish name is @setluca/mcp (the @setluca npm org). To publish: set NPM_TOKEN (scoped to @setluca), tag mcp-v*. See docs/release.md.",
98
+ "//publish": "Publish name is @setluca/mcp (the @setluca npm org). To publish: tag mcp-v*. The workflow authenticates to npm with OIDC Trusted Publishing, so there is no NPM_TOKEN. See docs/release.md.",
77
99
  "mcpName": "io.github.setluca/luca-mcp"
78
100
  }