@letterapp/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 letter.app
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @letterapp/mcp
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.
4
+
5
+ It reads the credential written by `npx @letterapp/cli` (`~/.letter/credentials.json`), so **your MCP config never contains a secret**.
6
+
7
+ ## Setup
8
+
9
+ First authenticate once from your project:
10
+
11
+ ```bash
12
+ npx @letterapp/cli
13
+ ```
14
+
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.
16
+
17
+ Then add the server to your MCP client (e.g. `mcp.json`):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "letter": {
23
+ "command": "npx",
24
+ "args": ["-y", "@letterapp/mcp"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
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).
31
+
32
+ ## Tools
33
+
34
+ | Tool | Description |
35
+ | --- | --- |
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`). |
41
+
42
+ ## Environment
43
+
44
+ | Variable | Default | Purpose |
45
+ | --- | --- | --- |
46
+ | `LETTER_API_KEY` | (from credential file) | Overrides the stored credential. |
47
+ | `LETTER_BASE_URL` | `https://api.letter.app` | API origin (self-host/dev). |
48
+ | `LETTER_DOCS_URL` | `https://letter.app` | Docs origin used by `setup_guide`. |
49
+
50
+ ## License
51
+
52
+ MIT
@@ -0,0 +1,41 @@
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
+ }
package/dist/index.js ADDED
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { docsBase, resolveCredential, } from "./credentials.js";
6
+ const VERSION = "0.1.0";
7
+ const USER_AGENT = "@letterapp/mcp";
8
+ function text(t, isError = false) {
9
+ return { content: [{ type: "text", text: t }], isError };
10
+ }
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);
12
+ 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 };
30
+ }
31
+ 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,
111
+ });
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);
136
+ }
137
+ main().catch((err) => {
138
+ process.stderr.write(`letter-mcp failed to start: ${err instanceof Error ? err.message : String(err)}\n`);
139
+ process.exit(1);
140
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@letterapp/mcp",
3
+ "version": "0.1.0",
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
+ "license": "MIT",
6
+ "homepage": "https://letter.app",
7
+ "author": "letter.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vincenzor/letter-mcp.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/vincenzor/letter-mcp/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "letter-mcp": "dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "keywords": [
28
+ "letter",
29
+ "letterapp",
30
+ "mcp",
31
+ "model-context-protocol",
32
+ "ai",
33
+ "agent"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
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"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.29.0",
46
+ "zod": "^3.24.1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.10.2",
50
+ "typescript": "^5.7.2"
51
+ }
52
+ }