@setluca/mcp 0.1.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.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Luca MCP
2
+
3
+ TypeScript MCP server for the Luca public API.
4
+
5
+ Luca MCP lets MCP clients call Luca's API-key-safe public API operations as
6
+ tools. It is intentionally narrow: it does not access Luca's database, browser
7
+ sessions, private webhook ingress, or `/internal/*` routes.
8
+
9
+ ## Status
10
+
11
+ - Transports: stdio (this npm package) and remote Streamable HTTP
12
+ (`src/worker.ts`, deployed as the `setluca-mcp` Cloudflare Worker at
13
+ `https://mcp.setluca.com/mcp`)
14
+ - Runtime: Bun (dev) — the built `dist/index.js` has no Bun-only APIs
15
+ - MCP SDK: `@modelcontextprotocol/sdk`
16
+ - Internal architecture: TypeScript, Effect services, Zod schemas
17
+ - Public API source of truth: `apps/api/openapi.json` (the checked-in
18
+ snapshot; `https://api.setluca.com/openapi.json` is the deployed copy)
19
+
20
+ ## Remote connection (hosted MCP clients)
21
+
22
+ Hosted clients (Claude connectors, ChatGPT) connect to
23
+ `https://mcp.setluca.com/mcp` with OAuth 2.1 — no API key pasting:
24
+
25
+ 1. The client discovers auth via
26
+ `https://mcp.setluca.com/.well-known/oauth-protected-resource/mcp`
27
+ (RFC 9728), which points at the Luca authorization server
28
+ (`https://api.setluca.com/api/auth`).
29
+ 2. It registers dynamically, then sends the coach through sign-in and a
30
+ scope-narrowing consent screen. Scopes: `luca:read` (baseline),
31
+ `luca:draft`, `luca:queue_ops`, `luca:full`, `luca:full_content`, plus
32
+ `offline_access` for refresh tokens. Defaults are read + draft.
33
+ 3. Coaches manage and revoke connections in Luca Settings → Connected agents.
34
+
35
+ Self-hosted/scripted clients can instead present a developer API key as the
36
+ bearer token on the same endpoint. Design and security model:
37
+ `docs/specs/mcp-oauth-authorization-server.md` (repo root).
38
+
39
+ ## Quickstart
40
+
41
+ > Not yet published to npm. The `npx` commands below work once the package is
42
+ > released — see [docs/release.md](./docs/release.md) for the remaining steps.
43
+ > Until then, build from source (further down).
44
+
45
+ Once published, add Luca to any MCP client (Claude Desktop, Cursor) with a
46
+ developer API key:
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "luca": {
52
+ "command": "npx",
53
+ "args": ["-y", "@setluca/mcp"],
54
+ "env": {
55
+ "LUCA_API_KEY": "luca_..."
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ Get a `LUCA_API_KEY` from Luca settings → developer API keys. The server needs
63
+ only that key; it lists tools without one and returns an actionable error on the
64
+ first tool call if it is missing.
65
+
66
+ ### From source (before the npm release)
67
+
68
+ From the repo root:
69
+
70
+ ```bash
71
+ bun install
72
+ bun --cwd apps/mcp run build
73
+ LUCA_API_KEY="luca_..." node apps/mcp/dist/index.js
74
+ ```
75
+
76
+ MCP client configuration against the built file:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "luca": {
82
+ "command": "node",
83
+ "args": ["/absolute/path/to/luca/apps/mcp/dist/index.js"],
84
+ "env": {
85
+ "LUCA_API_KEY": "luca_..."
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ## Configuration
93
+
94
+ Required:
95
+
96
+ ```bash
97
+ export LUCA_API_KEY="luca_..."
98
+ ```
99
+
100
+ Optional:
101
+
102
+ ```bash
103
+ export LUCA_API_BASE_URL="https://api.setluca.com"
104
+ export LUCA_AUTH_HEADER="x-api-key" # or "authorization"
105
+ export LUCA_WORKSPACE_ID="00000000-0000-0000-0000-000000000000"
106
+ export LUCA_WORKSPACE_SLUG="my-workspace"
107
+ ```
108
+
109
+ Every tool also accepts `workspaceId` and `workspaceSlug`, which override the
110
+ environment for that call.
111
+
112
+ ## Capabilities
113
+
114
+ - 67 Luca public API tools, each carrying MCP annotations (read-only /
115
+ destructive / idempotent hints)
116
+ - Resources: `luca://operations` (the tool manifest), `luca://lead/{leadId}`,
117
+ `luca://thread/{conversationId}`, and `luca://queue/today` — attach a lead,
118
+ thread, or the review queue as context without a tool round-trip
119
+ - Prompts: `luca-api-planner`, plus slash-command workflows
120
+ `luca-morning-report`, `luca-triage-queue`, and `luca-draft-reply`
121
+
122
+ Mutating tools include `idempotencyKey`. Provide a stable key when retrying the
123
+ same user intent; otherwise the server generates a unique key per call. Reads
124
+ that return lead-authored text are framed as untrusted content (see
125
+ [docs/security.md](./docs/security.md)).
126
+
127
+ ## Documentation
128
+
129
+ - [Architecture](./docs/architecture.md)
130
+ - [Client Setup](./docs/client-setup.md)
131
+ - [Configuration](./docs/configuration.md)
132
+ - [Development](./docs/development.md)
133
+ - [Examples](./docs/examples.md)
134
+ - [Integration Testing](./docs/integration-testing.md)
135
+ - [MCP Inspector](./docs/inspector.md)
136
+ - [Maintenance](./docs/maintenance.md)
137
+ - [Security Boundary](./docs/security.md)
138
+ - [Tool Reference](./docs/tools.md)
139
+
140
+ ## Development
141
+
142
+ From `apps/mcp` (or `bun --filter @setluca/mcp <script>` from the root):
143
+
144
+ ```bash
145
+ bun run docs:check
146
+ bun run verify
147
+ ```
148
+
149
+ `bun run verify` checks generated docs, OpenAPI input drift, the operation
150
+ manifest against `apps/api/openapi.json`, TypeScript, tests, build output, and
151
+ stdio startup. Lint, knip, and fallow run at the monorepo root.
152
+
153
+ ## API Docs
154
+
155
+ - Luca API docs: https://api.setluca.com/docs
156
+ - Luca OpenAPI JSON: https://api.setluca.com/openapi.json
package/dist/config.js ADDED
@@ -0,0 +1,47 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { LucaConfigError } from "./errors.js";
3
+ const TRAILING_SLASHES = /\/+$/;
4
+ export const LucaConfig = Context.GenericTag("@luca/mcp/LucaConfig");
5
+ function nonEmpty(value) {
6
+ const trimmed = value?.trim();
7
+ return trimmed ? trimmed : undefined;
8
+ }
9
+ function normalizeBaseUrl(value) {
10
+ const raw = nonEmpty(value) ?? "https://api.setluca.com";
11
+ return raw.replace(TRAILING_SLASHES, "");
12
+ }
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
+ });
24
+ }
25
+ export function loadConfig(env = process.env) {
26
+ const apiKey = nonEmpty(env.LUCA_API_KEY) ?? nonEmpty(env.LUCA_API_TOKEN);
27
+ if (!apiKey) {
28
+ throw new LucaConfigError({
29
+ message: "Set LUCA_API_KEY to a Luca developer API key.",
30
+ });
31
+ }
32
+ const workspaceId = nonEmpty(env.LUCA_WORKSPACE_ID);
33
+ const workspaceSlug = nonEmpty(env.LUCA_WORKSPACE_SLUG);
34
+ return {
35
+ apiBaseUrl: normalizeBaseUrl(env.LUCA_API_BASE_URL),
36
+ apiKey,
37
+ authHeader: parseAuthHeader(env.LUCA_AUTH_HEADER),
38
+ ...(workspaceId ? { workspaceId } : {}),
39
+ ...(workspaceSlug ? { workspaceSlug } : {}),
40
+ };
41
+ }
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
+ }));
package/dist/errors.js ADDED
@@ -0,0 +1,25 @@
1
+ import { Data } from "effect";
2
+ export class LucaConfigError extends Data.TaggedError("LucaConfigError") {
3
+ }
4
+ export class LucaNetworkError extends Data.TaggedError("LucaNetworkError") {
5
+ }
6
+ export class LucaDecodeError extends Data.TaggedError("LucaDecodeError") {
7
+ }
8
+ export class LucaHttpError extends Data.TaggedError("LucaHttpError") {
9
+ }
10
+ 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);
25
+ }