@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 +156 -0
- package/dist/config.js +47 -0
- package/dist/errors.js +25 -0
- package/dist/generated/openapi-inputs.js +2926 -0
- package/dist/generated/openapi-outputs.js +13798 -0
- package/dist/http.js +148 -0
- package/dist/index.js +12 -0
- package/dist/oauth-resolver.js +55 -0
- package/dist/observability.js +19 -0
- package/dist/openapi-zod.js +269 -0
- package/dist/operations.js +780 -0
- package/dist/prompts.js +79 -0
- package/dist/provenance.js +15 -0
- package/dist/remote.js +76 -0
- package/dist/resources.js +38 -0
- package/dist/server.js +375 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +74 -0
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
|
+
}
|