mursa-mcp 0.4.0 → 0.5.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 (3) hide show
  1. package/package.json +2 -8
  2. package/server.js +9 -2
  3. package/setup.js +275 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mursa-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Mursa MCP server — connect Claude, Cursor, and other MCP clients to your Mursa tasks, calendar, goals, notes, habits, projects, and Gmail.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "type": "commonjs",
10
10
  "files": [
11
11
  "server.js",
12
+ "setup.js",
12
13
  "README.md",
13
14
  "LICENSE"
14
15
  ],
@@ -30,13 +31,6 @@
30
31
  ],
31
32
  "license": "MIT",
32
33
  "homepage": "https://mursa.me/mcp",
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/Murali1889/MCP-mursa.git"
36
- },
37
- "bugs": {
38
- "url": "https://github.com/Murali1889/MCP-mursa/issues"
39
- },
40
34
  "dependencies": {
41
35
  "@modelcontextprotocol/sdk": "^1.29.0",
42
36
  "dotenv": "^16.4.5",
package/server.js CHANGED
@@ -20,6 +20,13 @@
20
20
  * Every tool below just maps to one edge-function action.
21
21
  */
22
22
 
23
+ // Subcommand: `npx mursa-mcp setup [...]` runs the installer instead of the
24
+ // MCP server. Detected before pulling in the SDK so setup stays fast.
25
+ if (process.argv[2] === "setup") {
26
+ require("./setup.js");
27
+ return;
28
+ }
29
+
23
30
  require("dotenv").config({ path: require("path").join(__dirname, ".env") });
24
31
 
25
32
  const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -30,7 +37,7 @@ const { z } = require("zod");
30
37
 
31
38
  // Default to the public Mursa proxy. Override only if you're self-hosting or
32
39
  // pointing at a preview deployment.
33
- const MURSA_API_URL = process.env.MURSA_API_URL || "https://mursa.me/api/mcp";
40
+ const MURSA_API_URL = process.env.MURSA_API_URL || "https://www.mursa.me/api/mcp";
34
41
  const MURSA_API_KEY = process.env.MURSA_API_KEY;
35
42
 
36
43
  if (!MURSA_API_KEY) {
@@ -83,7 +90,7 @@ function errorContent(err) {
83
90
  };
84
91
  }
85
92
 
86
- const server = new McpServer({ name: "mursa", version: "0.3.0" });
93
+ const server = new McpServer({ name: "mursa", version: "0.4.1" });
87
94
 
88
95
  function tool(name, description, schema, action) {
89
96
  server.tool(name, description, schema, async (args) => {
package/setup.js ADDED
@@ -0,0 +1,275 @@
1
+ // mursa-mcp setup — zero-friction installer.
2
+ //
3
+ // npx mursa-mcp setup --key=mursa_mcp_… # all defaults
4
+ // npx mursa-mcp setup --key=… --client=claude-code # specific client only
5
+ // npx mursa-mcp setup # interactive (prompts for key)
6
+ //
7
+ // Detects which MCP clients you have installed, safely merges the `mursa`
8
+ // entry into each one's config (preserving anything else already there), then
9
+ // runs a `whoami` call to verify the chain works end-to-end. Idempotent —
10
+ // re-running just updates the key.
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const os = require("os");
15
+ const readline = require("readline");
16
+
17
+ const ENDPOINT = process.env.MURSA_API_URL || "https://www.mursa.me/api/mcp";
18
+
19
+ // ───────────────────────────── colors ─────────────────────────────
20
+ const c = {
21
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
22
+ red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", cyan: "\x1b[36m",
23
+ };
24
+ const ok = (m) => console.log(`${c.green}✓${c.reset} ${m}`);
25
+ const warn = (m) => console.log(`${c.yellow}!${c.reset} ${m}`);
26
+ const err = (m) => console.log(`${c.red}✗${c.reset} ${m}`);
27
+ const info = (m) => console.log(`${c.dim}${m}${c.reset}`);
28
+ const heading = (m) => console.log(`\n${c.bold}${m}${c.reset}`);
29
+
30
+ // ───────────────────────────── clients ─────────────────────────────
31
+
32
+ const HOME = os.homedir();
33
+ const PLATFORM = process.platform;
34
+
35
+ const CLIENTS = [
36
+ {
37
+ id: "claude-code",
38
+ name: "Claude Code",
39
+ path: path.join(HOME, ".claude.json"),
40
+ root: "mcpServers",
41
+ style: "stdio",
42
+ },
43
+ {
44
+ id: "claude-desktop",
45
+ name: "Claude Desktop",
46
+ path: PLATFORM === "darwin"
47
+ ? path.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")
48
+ : PLATFORM === "win32"
49
+ ? path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json")
50
+ : path.join(HOME, ".config", "Claude", "claude_desktop_config.json"),
51
+ root: "mcpServers",
52
+ style: "stdio",
53
+ },
54
+ {
55
+ id: "cursor",
56
+ name: "Cursor",
57
+ path: path.join(HOME, ".cursor", "mcp.json"),
58
+ root: "mcpServers",
59
+ style: "stdio",
60
+ },
61
+ ];
62
+
63
+ function clientEntryFor(apiKey) {
64
+ const env = { MURSA_API_KEY: apiKey };
65
+ if (process.env.MURSA_API_URL) env.MURSA_API_URL = process.env.MURSA_API_URL;
66
+ return {
67
+ command: "npx",
68
+ args: ["-y", "mursa-mcp"],
69
+ env,
70
+ };
71
+ }
72
+
73
+ // ───────────────────────────── safe JSON merge ─────────────────────────────
74
+
75
+ function readJson(p) {
76
+ if (!fs.existsSync(p)) return null;
77
+ const raw = fs.readFileSync(p, "utf8").trim();
78
+ if (!raw) return {};
79
+ return JSON.parse(raw);
80
+ }
81
+
82
+ function writeJson(p, obj) {
83
+ fs.mkdirSync(path.dirname(p), { recursive: true });
84
+ fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n");
85
+ }
86
+
87
+ function backup(p) {
88
+ if (!fs.existsSync(p)) return null;
89
+ const bak = `${p}.bak.${Date.now()}`;
90
+ fs.copyFileSync(p, bak);
91
+ return bak;
92
+ }
93
+
94
+ function installInto(client, apiKey) {
95
+ let existed = fs.existsSync(client.path);
96
+ let cfg = existed ? readJson(client.path) : {};
97
+ cfg = cfg || {};
98
+ cfg[client.root] = cfg[client.root] || {};
99
+
100
+ const had = !!cfg[client.root].mursa;
101
+ const bak = existed ? backup(client.path) : null;
102
+
103
+ cfg[client.root].mursa = clientEntryFor(apiKey);
104
+ writeJson(client.path, cfg);
105
+
106
+ return {
107
+ path: client.path,
108
+ created: !existed,
109
+ updated: existed && had,
110
+ added: existed && !had,
111
+ backupAt: bak,
112
+ };
113
+ }
114
+
115
+ // ───────────────────────────── verify ─────────────────────────────
116
+
117
+ async function verify(apiKey) {
118
+ try {
119
+ const r = await fetch(ENDPOINT, {
120
+ method: "POST",
121
+ headers: {
122
+ Authorization: `Bearer ${apiKey}`,
123
+ "Content-Type": "application/json",
124
+ },
125
+ body: JSON.stringify({ action: "whoami", args: {} }),
126
+ });
127
+ const j = await r.json().catch(() => null);
128
+ if (!r.ok || j?.error) {
129
+ return { ok: false, message: j?.error || `HTTP ${r.status}` };
130
+ }
131
+ return { ok: true, userId: j?.data?.userId };
132
+ } catch (e) {
133
+ return { ok: false, message: e.message };
134
+ }
135
+ }
136
+
137
+ // ───────────────────────────── prompt ─────────────────────────────
138
+
139
+ function prompt(question, { mask = false } = {}) {
140
+ return new Promise((resolve) => {
141
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
142
+ if (!mask) {
143
+ rl.question(question, (a) => { rl.close(); resolve(a.trim()); });
144
+ } else {
145
+ // Best-effort masking for the key.
146
+ const orig = rl._writeToOutput;
147
+ rl._writeToOutput = function (s) { orig.call(rl, s.replace(/./g, (ch) => (ch === "\n" || ch === "\r" ? ch : "*"))); };
148
+ rl.question(question, (a) => { rl.close(); process.stdout.write("\n"); resolve(a.trim()); });
149
+ }
150
+ });
151
+ }
152
+
153
+ async function confirm(question, def = true) {
154
+ const a = (await prompt(`${question} ${def ? "[Y/n]" : "[y/N]"} `)).toLowerCase();
155
+ if (!a) return def;
156
+ return a === "y" || a === "yes";
157
+ }
158
+
159
+ // ───────────────────────────── args ─────────────────────────────
160
+
161
+ function parseArgs(argv) {
162
+ const out = {};
163
+ for (const a of argv) {
164
+ if (a === "--help" || a === "-h") out.help = true;
165
+ else if (a === "--yes" || a === "-y") out.yes = true;
166
+ else if (a.startsWith("--key=")) out.key = a.slice(6);
167
+ else if (a.startsWith("--client=")) out.client = a.slice(9);
168
+ }
169
+ return out;
170
+ }
171
+
172
+ function printHelp() {
173
+ console.log(`
174
+ ${c.bold}mursa-mcp setup${c.reset} — install the Mursa MCP into your AI client
175
+
176
+ ${c.bold}Usage${c.reset}
177
+ npx mursa-mcp setup --key=<key> # auto-detect + install
178
+ npx mursa-mcp setup --key=<key> --client=<id> # install into one client
179
+ npx mursa-mcp setup --key=<key> -y # no prompts
180
+
181
+ ${c.bold}Clients${c.reset}
182
+ claude-code Claude Code (~/.claude.json)
183
+ claude-desktop Claude Desktop
184
+ cursor Cursor
185
+
186
+ Get a key at ${c.cyan}https://dashboard.mursa.me${c.reset} → Settings → API keys.
187
+ `);
188
+ }
189
+
190
+ // ───────────────────────────── main ─────────────────────────────
191
+
192
+ async function main() {
193
+ const args = parseArgs(process.argv.slice(3));
194
+ if (args.help) { printHelp(); process.exit(0); }
195
+
196
+ heading("Mursa MCP setup");
197
+ info(`Endpoint: ${ENDPOINT}`);
198
+ info(`Platform: ${PLATFORM}`);
199
+
200
+ // 1. Resolve API key
201
+ let apiKey = args.key || process.env.MURSA_API_KEY;
202
+ if (!apiKey) {
203
+ console.log();
204
+ info("Get a key at https://dashboard.mursa.me → Settings → API keys");
205
+ apiKey = await prompt("Paste your MURSA_API_KEY: ", { mask: true });
206
+ }
207
+ if (!apiKey || !apiKey.startsWith("mursa_mcp_")) {
208
+ err("That doesn't look like a Mursa MCP key (should start with 'mursa_mcp_').");
209
+ process.exit(1);
210
+ }
211
+
212
+ // 2. Verify key before touching any files
213
+ heading("Verifying key");
214
+ const v = await verify(apiKey);
215
+ if (!v.ok) {
216
+ err(`Key check failed: ${v.message}`);
217
+ err("Aborting without changing any config.");
218
+ process.exit(1);
219
+ }
220
+ ok(`Key valid — userId ${v.userId}`);
221
+
222
+ // 3. Pick target clients
223
+ heading("Detecting installed clients");
224
+ const detected = CLIENTS.filter((c) => fs.existsSync(c.path));
225
+ const missing = CLIENTS.filter((c) => !fs.existsSync(c.path));
226
+
227
+ for (const c of detected) ok(`${c.name} ${info("("+c.path+")")}`);
228
+ for (const c of missing) warn(`${c.name} not detected ${info("("+c.path+")")}`);
229
+
230
+ let targets;
231
+ if (args.client) {
232
+ const t = CLIENTS.find((c) => c.id === args.client);
233
+ if (!t) { err(`Unknown --client=${args.client}. Use one of: ${CLIENTS.map((c)=>c.id).join(", ")}`); process.exit(1); }
234
+ targets = [t];
235
+ } else if (args.yes) {
236
+ targets = detected.length > 0 ? detected : [];
237
+ } else if (detected.length === 1) {
238
+ targets = detected;
239
+ } else if (detected.length > 1) {
240
+ heading("Install into?");
241
+ targets = [];
242
+ for (const c of detected) {
243
+ const y = await confirm(` Install Mursa MCP into ${c.name}?`, true);
244
+ if (y) targets.push(c);
245
+ }
246
+ } else {
247
+ warn("No MCP-compatible clients detected.");
248
+ const y = await confirm("Create config for Claude Code anyway?", true);
249
+ if (y) targets = [CLIENTS[0]];
250
+ }
251
+
252
+ if (!targets || targets.length === 0) {
253
+ warn("Nothing to install.");
254
+ process.exit(0);
255
+ }
256
+
257
+ // 4. Install
258
+ heading("Installing");
259
+ for (const t of targets) {
260
+ try {
261
+ const r = installInto(t, apiKey);
262
+ if (r.created) ok(`Created ${t.name} config ${info(r.path)}`);
263
+ else if (r.updated) ok(`Updated mursa entry in ${t.name} ${info("backup: " + r.backupAt)}`);
264
+ else ok(`Added mursa to ${t.name} ${info("backup: " + r.backupAt)}`);
265
+ } catch (e) {
266
+ err(`${t.name}: ${e.message}`);
267
+ }
268
+ }
269
+
270
+ heading("Done");
271
+ console.log(`Restart your client. The next session will have Mursa tools available.`);
272
+ console.log(`${c.dim}Try: "list my Mursa inbox" or call mursa.whoami${c.reset}`);
273
+ }
274
+
275
+ main().catch((e) => { err(e.message); process.exit(1); });