@letterapp/mcp 0.1.0 → 0.2.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 CHANGED
@@ -1,52 +1,124 @@
1
1
  # @letterapp/mcp
2
2
 
3
- A [Model Context Protocol](https://modelcontextprotocol.io) server that gives your AI coding agent tools to set up and verify a [Letter](https://letter.app) integration.
3
+ [![npm version](https://img.shields.io/npm/v/@letterapp/mcp)](https://www.npmjs.com/package/@letterapp/mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green)](./LICENSE)
4
5
 
5
- It reads the credential written by `npx @letterapp/cli` (`~/.letter/credentials.json`), so **your MCP config never contains a secret**.
6
+ **Letter MCP server** a [Model Context Protocol](https://modelcontextprotocol.io) server that gives your AI coding agent tools to set up and verify a [Letter](https://letter.app) integration.
6
7
 
7
- ## Setup
8
-
9
- First authenticate once from your project:
8
+ Built for AI coding agents (Claude Code, Cursor, Codex, Windsurf, Cline, OpenClaw). It reads the credential written by [`@letterapp/cli`](https://www.npmjs.com/package/@letterapp/cli) (`~/.letter/credentials.json`), so **your MCP config never contains a secret** and the key never appears in chat.
10
9
 
11
- ```bash
12
- npx @letterapp/cli
13
- ```
10
+ ---
14
11
 
15
- This opens a browser to confirm and writes `LETTER_API_KEY` to `.env.local` plus `~/.letter/credentials.json` (used by this server). The key is never printed to your terminal or chat.
12
+ ## Installation
16
13
 
17
- Then add the server to your MCP client (e.g. `mcp.json`):
14
+ No install needed - your MCP client launches it on demand with `npx`. Add it to your client config:
18
15
 
19
16
  ```json
20
17
  {
21
18
  "mcpServers": {
22
- "letter": {
23
- "command": "npx",
24
- "args": ["-y", "@letterapp/mcp"]
25
- }
19
+ "letter": { "command": "npx", "args": ["-y", "@letterapp/mcp"] }
26
20
  }
27
21
  }
28
22
  ```
29
23
 
30
- No API key in the config. The server resolves it from `~/.letter/credentials.json`, or from `LETTER_API_KEY` in the environment (handy for CI).
24
+ Or install globally:
25
+
26
+ ```bash
27
+ npm install -g @letterapp/mcp
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Setup
33
+
34
+ First authenticate once from your project (this is what writes the credential the server reads):
35
+
36
+ ```bash
37
+ npx @letterapp/cli
38
+ ```
39
+
40
+ What happens:
41
+
42
+ 1. The CLI opens a browser to confirm a short code and pick the project to connect.
43
+ 2. Letter mints a project API key and delivers it to the CLI over a secure back channel. The CLI writes `LETTER_API_KEY` to `.env.local` and stores a copy in `~/.letter/credentials.json`.
44
+ 3. This server resolves that credential automatically - no API key in your MCP config.
45
+
46
+ The API key is never printed to the terminal or chat. In CI you can instead set `LETTER_API_KEY` in the environment.
47
+
48
+ ---
31
49
 
32
50
  ## Tools
33
51
 
34
52
  | Tool | Description |
35
53
  | --- | --- |
36
- | `setup_guide` | Returns Letter's step-by-step integration guide (markdown). |
37
- | `check_connection` | Reports whether the connected project has received any contacts or events. |
38
- | `send_test_event` | Sends a test `identify` + `track` to prove the pipe works. |
39
- | `identify` | Report a user (`userId`, optional `email`, `traits`). |
40
- | `track` | Report an event (`userId`, `event`, optional `properties`). |
54
+ | `setup_guide` | Returns Letter's current step-by-step integration guide (markdown an agent can follow). |
55
+ | `check_connection` | Reports whether the connected project has received any contacts or events yet. |
56
+ | `send_test_event` | Sends a test `identify` + `track` to prove the pipe works end to end. Optional `userId` / `email`. |
57
+ | `identify` | Report a user to Letter (`userId`, optional `email`, `traits`). |
58
+ | `track` | Report an event a user performed (`userId`, `event`, optional `properties`). |
59
+
60
+ When no credential is found, every tool returns a friendly "run `npx @letterapp/cli` first" message instead of failing.
61
+
62
+ ---
41
63
 
42
- ## Environment
64
+ ## Environment Variables
43
65
 
44
66
  | Variable | Default | Purpose |
45
67
  | --- | --- | --- |
46
- | `LETTER_API_KEY` | (from credential file) | Overrides the stored credential. |
47
- | `LETTER_BASE_URL` | `https://api.letter.app` | API origin (self-host/dev). |
68
+ | `LETTER_API_KEY` | (from credential file) | Overrides the stored credential (CI). |
69
+ | `LETTER_BASE_URL` | `https://api.letter.app` | API origin for self-host / local dev. |
48
70
  | `LETTER_DOCS_URL` | `https://letter.app` | Docs origin used by `setup_guide`. |
49
71
 
72
+ ---
73
+
74
+ ## Security
75
+
76
+ The server never receives or stores a raw key in its config. It resolves the credential from `~/.letter/credentials.json` (written by the CLI's browser-approved device flow) or from `LETTER_API_KEY` in the environment. Secrets are never logged or echoed back through tool results. Treat `.env.local` and `~/.letter/credentials.json` as secrets and keep them out of source control.
77
+
78
+ ---
79
+
80
+ ## Project Structure
81
+
82
+ ```
83
+ src/
84
+ ├── index.ts # MCP server entry — registers tools over stdio
85
+ └── credentials.ts # Resolves the Letter credential (env or ~/.letter)
86
+ package.json
87
+ tsconfig.json
88
+ tsup.config.ts
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ npm install # install dependencies
97
+ npm run build # bundle to dist/ with tsup
98
+ npm run dev # watch mode
99
+ npm run typecheck
100
+
101
+ node dist/index.js # run the server over stdio
102
+ ```
103
+
104
+ ---
105
+
106
+ ## See Also
107
+
108
+ - [`@letterapp/cli`](https://www.npmjs.com/package/@letterapp/cli) — connect your app to Letter in one command.
109
+ - [`@letterapp/node`](https://www.npmjs.com/package/@letterapp/node) — Node.js SDK.
110
+
111
+ ---
112
+
113
+ ## Links
114
+
115
+ - **Website:** [letter.app](https://letter.app)
116
+ - **npm:** [@letterapp/mcp](https://www.npmjs.com/package/@letterapp/mcp)
117
+ - **GitHub:** [vincenzor/letter-mcp](https://github.com/vincenzor/letter-mcp)
118
+ - **Issues:** [Report a bug](https://github.com/vincenzor/letter-mcp/issues)
119
+
120
+ ---
121
+
50
122
  ## License
51
123
 
52
124
  MIT
package/dist/index.js CHANGED
@@ -1,140 +1,196 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // src/index.ts
2
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
6
  import { z } from "zod";
5
- import { docsBase, resolveCredential, } from "./credentials.js";
6
- const VERSION = "0.1.0";
7
- const USER_AGENT = "@letterapp/mcp";
7
+
8
+ // src/credentials.ts
9
+ import { homedir } from "os";
10
+ import path from "path";
11
+ import { readFile } from "fs/promises";
12
+ var DEFAULT_API_BASE = "https://api.letter.app";
13
+ var DEFAULT_DOCS_BASE = "https://letter.app";
14
+ async function resolveCredential() {
15
+ const envKey = process.env.LETTER_API_KEY;
16
+ if (envKey) {
17
+ return {
18
+ apiKey: envKey,
19
+ baseUrl: (process.env.LETTER_BASE_URL || DEFAULT_API_BASE).replace(
20
+ /\/$/,
21
+ ""
22
+ )
23
+ };
24
+ }
25
+ try {
26
+ const file = path.join(homedir(), ".letter", "credentials.json");
27
+ const parsed = JSON.parse(await readFile(file, "utf8"));
28
+ if (!parsed.apiKey) return null;
29
+ return {
30
+ apiKey: parsed.apiKey,
31
+ baseUrl: (parsed.baseUrl || DEFAULT_API_BASE).replace(/\/$/, ""),
32
+ project: parsed.project
33
+ };
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function docsBase() {
39
+ return (process.env.LETTER_DOCS_URL || DEFAULT_DOCS_BASE).replace(/\/$/, "");
40
+ }
41
+
42
+ // src/index.ts
43
+ var VERSION = "0.2.0";
44
+ var USER_AGENT = "@letterapp/mcp";
8
45
  function text(t, isError = false) {
9
- return { content: [{ type: "text", text: t }], isError };
46
+ return { content: [{ type: "text", text: t }], isError };
10
47
  }
11
- const NOT_CONNECTED = text("No Letter credential found. Run `npx @letterapp/cli` in your project to log in (it writes ~/.letter/credentials.json), or set LETTER_API_KEY in the environment. The key is provisioned via a browser confirmation - never paste it into chat.", true);
48
+ var NOT_CONNECTED = text(
49
+ "No Letter credential found. Run `npx @letterapp/cli` in your project to log in (it writes ~/.letter/credentials.json), or set LETTER_API_KEY in the environment. The key is provisioned via a browser confirmation - never paste it into chat.",
50
+ true
51
+ );
12
52
  async function api(cred, method, pathname, body) {
13
- const res = await fetch(`${cred.baseUrl}${pathname}`, {
14
- method,
15
- headers: {
16
- authorization: `Bearer ${cred.apiKey}`,
17
- "content-type": "application/json",
18
- "user-agent": USER_AGENT,
19
- },
20
- body: body === undefined ? undefined : JSON.stringify(body),
21
- });
22
- let json = null;
23
- try {
24
- json = await res.json();
25
- }
26
- catch {
27
- json = null;
28
- }
29
- return { ok: res.ok, status: res.status, json };
53
+ const res = await fetch(`${cred.baseUrl}${pathname}`, {
54
+ method,
55
+ headers: {
56
+ authorization: `Bearer ${cred.apiKey}`,
57
+ "content-type": "application/json",
58
+ "user-agent": USER_AGENT
59
+ },
60
+ body: body === void 0 ? void 0 : JSON.stringify(body)
61
+ });
62
+ let json = null;
63
+ try {
64
+ json = await res.json();
65
+ } catch {
66
+ json = null;
67
+ }
68
+ return { ok: res.ok, status: res.status, json };
30
69
  }
31
70
  async function main() {
32
- const server = new McpServer({ name: "letter", version: VERSION });
33
- // setup_guide: hand the agent the full, current integration steps.
34
- server.tool("setup_guide", "Get Letter's step-by-step integration guide (install, authenticate, instrument identify/track). Returns markdown an agent can follow.", {}, async () => {
35
- try {
36
- const res = await fetch(`${docsBase()}/docs/agent-setup/raw`, {
37
- headers: { "user-agent": USER_AGENT },
38
- });
39
- if (res.ok)
40
- return text(await res.text());
41
- }
42
- catch {
43
- // fall through to the built-in summary
44
- }
45
- return text([
46
- "# Letter setup",
47
- "1. Run `npx @letterapp/cli` in the project root to authenticate (browser confirm) and install @letterapp/node. It writes LETTER_API_KEY to .env.local.",
48
- "2. Create a server-side client: `new Letter({ apiKey: process.env.LETTER_API_KEY! })`.",
49
- "3. Call `letter.identify({ userId, email, traits })` where users sign up/log in.",
50
- "4. Call `letter.track({ userId, event })` on 2-3 key actions.",
51
- "5. Verify with the `check_connection` tool.",
52
- "Full docs: https://letter.app/docs/agent-setup",
53
- ].join("\n"));
54
- });
55
- // check_connection: confirm the project has received data.
56
- server.tool("check_connection", "Check whether the connected Letter project has received any contacts or events yet. Use to verify the integration is live.", {}, async () => {
57
- const cred = await resolveCredential();
58
- if (!cred)
59
- return NOT_CONNECTED;
60
- const { ok, status, json } = await api(cred, "GET", "/v1/status");
61
- if (!ok) {
62
- return text(`Could not reach Letter (HTTP ${status}).`, true);
63
- }
64
- const data = json;
65
- const name = data.project?.name ?? cred.project?.name ?? "your project";
66
- return text(data.connected
67
- ? `Connected. ${name} has ${data.contacts ?? 0} contact(s) and ${data.events ?? 0} event(s).`
68
- : `${name} is set up but hasn't received any data yet. Trigger an identify/track call (or use send_test_event).`);
69
- });
70
- // send_test_event: prove the pipe end to end.
71
- server.tool("send_test_event", "Send a test identify + track to Letter to prove the connection works. Optionally pass a userId/email.", {
72
- userId: z.string().optional(),
73
- email: z.string().optional(),
74
- }, async ({ userId, email }) => {
75
- const cred = await resolveCredential();
76
- if (!cred)
77
- return NOT_CONNECTED;
78
- const uid = userId || `mcp_test_${Date.now()}`;
79
- const mail = email || `${uid}@example.com`;
80
- const idRes = await api(cred, "POST", "/v1/identify", {
81
- userId: uid,
82
- email: mail,
83
- traits: { name: "MCP Test", source: "mcp" },
84
- });
85
- if (!idRes.ok) {
86
- return text(`identify failed (HTTP ${idRes.status}).`, true);
87
- }
88
- const trackRes = await api(cred, "POST", "/v1/track", {
89
- userId: uid,
90
- event: "MCP Test Event",
91
- properties: { via: "@letterapp/mcp" },
92
- });
93
- if (!trackRes.ok) {
94
- return text(`track failed (HTTP ${trackRes.status}).`, true);
95
- }
96
- return text(`Sent a test contact (${uid}) and a "MCP Test Event". Check your Letter dashboard, or run check_connection.`);
97
- });
98
- // identify passthrough.
99
- server.tool("identify", "Report a user to Letter (Segment-style identify). Use a stable userId.", {
100
- userId: z.string(),
101
- email: z.string().optional(),
102
- traits: z.record(z.any()).optional(),
103
- }, async ({ userId, email, traits }) => {
104
- const cred = await resolveCredential();
105
- if (!cred)
106
- return NOT_CONNECTED;
107
- const res = await api(cred, "POST", "/v1/identify", {
108
- userId,
109
- email,
110
- traits,
71
+ const server = new McpServer({ name: "letter", version: VERSION });
72
+ server.tool(
73
+ "setup_guide",
74
+ "Get Letter's step-by-step integration guide (install, authenticate, instrument identify/track). Returns markdown an agent can follow.",
75
+ {},
76
+ async () => {
77
+ try {
78
+ const res = await fetch(`${docsBase()}/docs/agent-setup/raw`, {
79
+ headers: { "user-agent": USER_AGENT }
111
80
  });
112
- return res.ok
113
- ? text(`Identified ${userId}.`)
114
- : text(`identify failed (HTTP ${res.status}).`, true);
115
- });
116
- // track passthrough.
117
- server.tool("track", "Report an event a user performed to Letter (Segment-style track).", {
118
- userId: z.string(),
119
- event: z.string(),
120
- properties: z.record(z.any()).optional(),
121
- }, async ({ userId, event, properties }) => {
122
- const cred = await resolveCredential();
123
- if (!cred)
124
- return NOT_CONNECTED;
125
- const res = await api(cred, "POST", "/v1/track", {
126
- userId,
127
- event,
128
- properties,
129
- });
130
- return res.ok
131
- ? text(`Tracked "${event}" for ${userId}.`)
132
- : text(`track failed (HTTP ${res.status}).`, true);
133
- });
134
- const transport = new StdioServerTransport();
135
- await server.connect(transport);
81
+ if (res.ok) return text(await res.text());
82
+ } catch {
83
+ }
84
+ return text(
85
+ [
86
+ "# Letter setup",
87
+ "1. Run `npx @letterapp/cli` in the project root to authenticate (browser confirm) and install @letterapp/node. It writes LETTER_API_KEY to .env.local.",
88
+ "2. Create a server-side client: `new Letter({ apiKey: process.env.LETTER_API_KEY! })`.",
89
+ "3. Call `letter.identify({ userId, email, traits })` where users sign up/log in.",
90
+ "4. Call `letter.track({ userId, event })` on 2-3 key actions.",
91
+ "5. Verify with the `check_connection` tool.",
92
+ "Full docs: https://letter.app/docs/agent-setup"
93
+ ].join("\n")
94
+ );
95
+ }
96
+ );
97
+ server.tool(
98
+ "check_connection",
99
+ "Check whether the connected Letter project has received any contacts or events yet. Use to verify the integration is live.",
100
+ {},
101
+ async () => {
102
+ const cred = await resolveCredential();
103
+ if (!cred) return NOT_CONNECTED;
104
+ const { ok, status, json } = await api(cred, "GET", "/v1/status");
105
+ if (!ok) {
106
+ return text(`Could not reach Letter (HTTP ${status}).`, true);
107
+ }
108
+ const data = json;
109
+ const name = data.project?.name ?? cred.project?.name ?? "your project";
110
+ return text(
111
+ data.connected ? `Connected. ${name} has ${data.contacts ?? 0} contact(s) and ${data.events ?? 0} event(s).` : `${name} is set up but hasn't received any data yet. Trigger an identify/track call (or use send_test_event).`
112
+ );
113
+ }
114
+ );
115
+ server.tool(
116
+ "send_test_event",
117
+ "Send a test identify + track to Letter to prove the connection works. Optionally pass a userId/email.",
118
+ {
119
+ userId: z.string().optional(),
120
+ email: z.string().optional()
121
+ },
122
+ async ({ userId, email }) => {
123
+ const cred = await resolveCredential();
124
+ if (!cred) return NOT_CONNECTED;
125
+ const uid = userId || `mcp_test_${Date.now()}`;
126
+ const mail = email || `${uid}@example.com`;
127
+ const idRes = await api(cred, "POST", "/v1/identify", {
128
+ userId: uid,
129
+ email: mail,
130
+ traits: { name: "MCP Test", source: "mcp" }
131
+ });
132
+ if (!idRes.ok) {
133
+ return text(`identify failed (HTTP ${idRes.status}).`, true);
134
+ }
135
+ const trackRes = await api(cred, "POST", "/v1/track", {
136
+ userId: uid,
137
+ event: "MCP Test Event",
138
+ properties: { via: "@letterapp/mcp" }
139
+ });
140
+ if (!trackRes.ok) {
141
+ return text(`track failed (HTTP ${trackRes.status}).`, true);
142
+ }
143
+ return text(
144
+ `Sent a test contact (${uid}) and a "MCP Test Event". Check your Letter dashboard, or run check_connection.`
145
+ );
146
+ }
147
+ );
148
+ server.tool(
149
+ "identify",
150
+ "Report a user to Letter (Segment-style identify). Use a stable userId.",
151
+ {
152
+ userId: z.string(),
153
+ email: z.string().optional(),
154
+ traits: z.record(z.any()).optional()
155
+ },
156
+ async ({ userId, email, traits }) => {
157
+ const cred = await resolveCredential();
158
+ if (!cred) return NOT_CONNECTED;
159
+ const res = await api(cred, "POST", "/v1/identify", {
160
+ userId,
161
+ email,
162
+ traits
163
+ });
164
+ return res.ok ? text(`Identified ${userId}.`) : text(`identify failed (HTTP ${res.status}).`, true);
165
+ }
166
+ );
167
+ server.tool(
168
+ "track",
169
+ "Report an event a user performed to Letter (Segment-style track).",
170
+ {
171
+ userId: z.string(),
172
+ event: z.string(),
173
+ properties: z.record(z.any()).optional()
174
+ },
175
+ async ({ userId, event, properties }) => {
176
+ const cred = await resolveCredential();
177
+ if (!cred) return NOT_CONNECTED;
178
+ const res = await api(cred, "POST", "/v1/track", {
179
+ userId,
180
+ event,
181
+ properties
182
+ });
183
+ return res.ok ? text(`Tracked "${event}" for ${userId}.`) : text(`track failed (HTTP ${res.status}).`, true);
184
+ }
185
+ );
186
+ const transport = new StdioServerTransport();
187
+ await server.connect(transport);
136
188
  }
137
189
  main().catch((err) => {
138
- process.stderr.write(`letter-mcp failed to start: ${err instanceof Error ? err.message : String(err)}\n`);
139
- process.exit(1);
190
+ process.stderr.write(
191
+ `letter-mcp failed to start: ${err instanceof Error ? err.message : String(err)}
192
+ `
193
+ );
194
+ process.exit(1);
140
195
  });
196
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/credentials.ts"],"sourcesContent":["declare const PKG_VERSION: string;\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport {\n type Credential,\n docsBase,\n resolveCredential,\n} from \"./credentials.js\";\n\nconst VERSION = PKG_VERSION;\nconst USER_AGENT = \"@letterapp/mcp\";\n\ntype TextResult = {\n content: { type: \"text\"; text: string }[];\n isError?: boolean;\n};\n\nfunction text(t: string, isError = false): TextResult {\n return { content: [{ type: \"text\", text: t }], isError };\n}\n\nconst NOT_CONNECTED = text(\n \"No Letter credential found. Run `npx @letterapp/cli` in your project to log in (it writes ~/.letter/credentials.json), or set LETTER_API_KEY in the environment. The key is provisioned via a browser confirmation - never paste it into chat.\",\n true,\n);\n\nasync function api(\n cred: Credential,\n method: \"GET\" | \"POST\",\n pathname: string,\n body?: unknown,\n): Promise<{ ok: boolean; status: number; json: unknown }> {\n const res = await fetch(`${cred.baseUrl}${pathname}`, {\n method,\n headers: {\n authorization: `Bearer ${cred.apiKey}`,\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n let json: unknown = null;\n try {\n json = await res.json();\n } catch {\n json = null;\n }\n return { ok: res.ok, status: res.status, json };\n}\n\nasync function main(): Promise<void> {\n const server = new McpServer({ name: \"letter\", version: VERSION });\n\n // setup_guide: hand the agent the full, current integration steps.\n server.tool(\n \"setup_guide\",\n \"Get Letter's step-by-step integration guide (install, authenticate, instrument identify/track). Returns markdown an agent can follow.\",\n {},\n async () => {\n try {\n const res = await fetch(`${docsBase()}/docs/agent-setup/raw`, {\n headers: { \"user-agent\": USER_AGENT },\n });\n if (res.ok) return text(await res.text());\n } catch {\n // fall through to the built-in summary\n }\n return text(\n [\n \"# Letter setup\",\n \"1. Run `npx @letterapp/cli` in the project root to authenticate (browser confirm) and install @letterapp/node. It writes LETTER_API_KEY to .env.local.\",\n \"2. Create a server-side client: `new Letter({ apiKey: process.env.LETTER_API_KEY! })`.\",\n \"3. Call `letter.identify({ userId, email, traits })` where users sign up/log in.\",\n \"4. Call `letter.track({ userId, event })` on 2-3 key actions.\",\n \"5. Verify with the `check_connection` tool.\",\n \"Full docs: https://letter.app/docs/agent-setup\",\n ].join(\"\\n\"),\n );\n },\n );\n\n // check_connection: confirm the project has received data.\n server.tool(\n \"check_connection\",\n \"Check whether the connected Letter project has received any contacts or events yet. Use to verify the integration is live.\",\n {},\n async () => {\n const cred = await resolveCredential();\n if (!cred) return NOT_CONNECTED;\n const { ok, status, json } = await api(cred, \"GET\", \"/v1/status\");\n if (!ok) {\n return text(`Could not reach Letter (HTTP ${status}).`, true);\n }\n const data = json as {\n contacts?: number;\n events?: number;\n connected?: boolean;\n project?: { name?: string };\n };\n const name = data.project?.name ?? cred.project?.name ?? \"your project\";\n return text(\n data.connected\n ? `Connected. ${name} has ${data.contacts ?? 0} contact(s) and ${data.events ?? 0} event(s).`\n : `${name} is set up but hasn't received any data yet. Trigger an identify/track call (or use send_test_event).`,\n );\n },\n );\n\n // send_test_event: prove the pipe end to end.\n server.tool(\n \"send_test_event\",\n \"Send a test identify + track to Letter to prove the connection works. Optionally pass a userId/email.\",\n {\n userId: z.string().optional(),\n email: z.string().optional(),\n },\n async ({ userId, email }) => {\n const cred = await resolveCredential();\n if (!cred) return NOT_CONNECTED;\n const uid = userId || `mcp_test_${Date.now()}`;\n const mail = email || `${uid}@example.com`;\n\n const idRes = await api(cred, \"POST\", \"/v1/identify\", {\n userId: uid,\n email: mail,\n traits: { name: \"MCP Test\", source: \"mcp\" },\n });\n if (!idRes.ok) {\n return text(`identify failed (HTTP ${idRes.status}).`, true);\n }\n const trackRes = await api(cred, \"POST\", \"/v1/track\", {\n userId: uid,\n event: \"MCP Test Event\",\n properties: { via: \"@letterapp/mcp\" },\n });\n if (!trackRes.ok) {\n return text(`track failed (HTTP ${trackRes.status}).`, true);\n }\n return text(\n `Sent a test contact (${uid}) and a \"MCP Test Event\". Check your Letter dashboard, or run check_connection.`,\n );\n },\n );\n\n // identify passthrough.\n server.tool(\n \"identify\",\n \"Report a user to Letter (Segment-style identify). Use a stable userId.\",\n {\n userId: z.string(),\n email: z.string().optional(),\n traits: z.record(z.any()).optional(),\n },\n async ({ userId, email, traits }) => {\n const cred = await resolveCredential();\n if (!cred) return NOT_CONNECTED;\n const res = await api(cred, \"POST\", \"/v1/identify\", {\n userId,\n email,\n traits,\n });\n return res.ok\n ? text(`Identified ${userId}.`)\n : text(`identify failed (HTTP ${res.status}).`, true);\n },\n );\n\n // track passthrough.\n server.tool(\n \"track\",\n \"Report an event a user performed to Letter (Segment-style track).\",\n {\n userId: z.string(),\n event: z.string(),\n properties: z.record(z.any()).optional(),\n },\n async ({ userId, event, properties }) => {\n const cred = await resolveCredential();\n if (!cred) return NOT_CONNECTED;\n const res = await api(cred, \"POST\", \"/v1/track\", {\n userId,\n event,\n properties,\n });\n return res.ok\n ? text(`Tracked \"${event}\" for ${userId}.`)\n : text(`track failed (HTTP ${res.status}).`, true);\n },\n );\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((err) => {\n process.stderr.write(\n `letter-mcp failed to start: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n process.exit(1);\n});\n","import { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { readFile } from \"node:fs/promises\";\n\nexport const DEFAULT_API_BASE = \"https://api.letter.app\";\nexport const DEFAULT_DOCS_BASE = \"https://letter.app\";\n\nexport type Credential = {\n apiKey: string;\n baseUrl: string;\n project?: { slug: string; name: string };\n};\n\ntype StoredCredential = {\n apiKey: string;\n baseUrl?: string;\n project?: { slug: string; name: string };\n};\n\n/**\n * Resolves the Letter credential for the MCP server, in priority order:\n * 1. LETTER_API_KEY (+ LETTER_BASE_URL) from the environment.\n * 2. ~/.letter/credentials.json written by `npx @letterapp/cli`.\n *\n * Returns null when neither is present, so tools can return a friendly\n * \"run `npx @letterapp/cli` first\" message instead of crashing. The secret is\n * never logged or echoed back through tool results.\n */\nexport async function resolveCredential(): Promise<Credential | null> {\n const envKey = process.env.LETTER_API_KEY;\n if (envKey) {\n return {\n apiKey: envKey,\n baseUrl: (process.env.LETTER_BASE_URL || DEFAULT_API_BASE).replace(\n /\\/$/,\n \"\",\n ),\n };\n }\n\n try {\n const file = path.join(homedir(), \".letter\", \"credentials.json\");\n const parsed = JSON.parse(await readFile(file, \"utf8\")) as StoredCredential;\n if (!parsed.apiKey) return null;\n return {\n apiKey: parsed.apiKey,\n baseUrl: (parsed.baseUrl || DEFAULT_API_BASE).replace(/\\/$/, \"\"),\n project: parsed.project,\n };\n } catch {\n return null;\n }\n}\n\n/** Docs origin for fetching the agent-setup guide. */\nexport function docsBase(): string {\n return (process.env.LETTER_DOCS_URL || DEFAULT_DOCS_BASE).replace(/\\/$/, \"\");\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterapp/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Letter MCP server - give your AI agent tools to set up and verify a Letter integration. Reads the credential written by `letter login`; no secret in your MCP config.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://letter.app",
@@ -36,10 +36,10 @@
36
36
  "access": "public"
37
37
  },
38
38
  "scripts": {
39
- "build": "tsc -p tsconfig.json",
40
- "clean": "rm -rf dist tsconfig.tsbuildinfo",
41
- "typecheck": "tsc --noEmit",
42
- "prepublishOnly": "npm run clean && npm run build"
39
+ "dev": "tsup --watch",
40
+ "build": "tsup",
41
+ "start": "node dist/index.js",
42
+ "typecheck": "tsc --noEmit"
43
43
  },
44
44
  "dependencies": {
45
45
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -47,6 +47,7 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^22.10.2",
50
+ "tsup": "^8.4.0",
50
51
  "typescript": "^5.7.2"
51
52
  }
52
53
  }
@@ -1,41 +0,0 @@
1
- import { homedir } from "node:os";
2
- import path from "node:path";
3
- import { readFile } from "node:fs/promises";
4
- export const DEFAULT_API_BASE = "https://api.letter.app";
5
- export const DEFAULT_DOCS_BASE = "https://letter.app";
6
- /**
7
- * Resolves the Letter credential for the MCP server, in priority order:
8
- * 1. LETTER_API_KEY (+ LETTER_BASE_URL) from the environment.
9
- * 2. ~/.letter/credentials.json written by `npx @letterapp/cli`.
10
- *
11
- * Returns null when neither is present, so tools can return a friendly
12
- * "run `npx @letterapp/cli` first" message instead of crashing. The secret is
13
- * never logged or echoed back through tool results.
14
- */
15
- export async function resolveCredential() {
16
- const envKey = process.env.LETTER_API_KEY;
17
- if (envKey) {
18
- return {
19
- apiKey: envKey,
20
- baseUrl: (process.env.LETTER_BASE_URL || DEFAULT_API_BASE).replace(/\/$/, ""),
21
- };
22
- }
23
- try {
24
- const file = path.join(homedir(), ".letter", "credentials.json");
25
- const parsed = JSON.parse(await readFile(file, "utf8"));
26
- if (!parsed.apiKey)
27
- return null;
28
- return {
29
- apiKey: parsed.apiKey,
30
- baseUrl: (parsed.baseUrl || DEFAULT_API_BASE).replace(/\/$/, ""),
31
- project: parsed.project,
32
- };
33
- }
34
- catch {
35
- return null;
36
- }
37
- }
38
- /** Docs origin for fetching the agent-setup guide. */
39
- export function docsBase() {
40
- return (process.env.LETTER_DOCS_URL || DEFAULT_DOCS_BASE).replace(/\/$/, "");
41
- }