easymd-cli 0.1.0 → 0.1.1
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 +15 -3
- package/bin/easymd.js +21 -1
- package/package.json +8 -3
- package/src/cli/auth.js +10 -1
- package/src/cli/mcp-install.js +48 -0
- package/src/cli/mcp.mjs +125 -0
package/README.md
CHANGED
|
@@ -69,12 +69,24 @@ with `EASYMD_URL=https://your-easymd.example.com`.
|
|
|
69
69
|
|
|
70
70
|
easymd ships an MCP server so AI agents edit the **same live documents** humans do — changes sync in real time and persist to Supabase.
|
|
71
71
|
|
|
72
|
+
The easiest way is the CLI's bundled MCP server. It authenticates with your `easymd login` token (no API keys to paste) and works with any agent:
|
|
73
|
+
|
|
72
74
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
npm i -g easymd-cli
|
|
76
|
+
easymd login
|
|
77
|
+
easymd mcp-install # registers it with Cursor + Claude Desktop
|
|
78
|
+
# Claude Code: claude mcp add easymd -- easymd mcp
|
|
75
79
|
```
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
Or use [`add-mcp`](https://github.com/neondatabase-labs/add-mcp) to register it across **all** detected agents (Cursor, Claude Desktop, VS Code, …) in one command:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx add-mcp@latest --command easymd --args "mcp" --name easymd
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Tools: `list_documents`, `read_document`, `create_document`, `update_document`, `append_to_document`. Docs an agent creates show up in your dashboard; edits an agent makes appear live in any open editor.
|
|
88
|
+
|
|
89
|
+
(The repo also has a server-side MCP at `web/scripts/mcp-server.mjs` — `npm run mcp` — which uses the service-role key directly; prefer the CLI version for end users.)
|
|
78
90
|
|
|
79
91
|
## Why
|
|
80
92
|
|
package/bin/easymd.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
import { access } from 'fs/promises';
|
|
5
5
|
import openBrowser from 'open';
|
|
6
|
-
import { startServer } from '../src/server.js';
|
|
7
6
|
import { login, logout, whoami } from '../src/cli/auth.js';
|
|
8
7
|
import { syncDir, watchDir } from '../src/cli/sync.js';
|
|
9
8
|
import { autoOn, autoOff, autoStatus } from '../src/cli/auto.js';
|
|
10
9
|
import { getCredentials } from '../src/cli/config.js';
|
|
10
|
+
import { mcpInstall } from '../src/cli/mcp-install.js';
|
|
11
11
|
|
|
12
12
|
const HELP = `
|
|
13
13
|
easymd — collaborate on markdown files in your repo, live with humans and AI agents
|
|
@@ -24,6 +24,9 @@ Usage:
|
|
|
24
24
|
easymd sync [dir] One-shot: push all .md files under dir (default: .) to your account
|
|
25
25
|
easymd watch [dir] Foreground watcher (what 'auto on' runs in the background)
|
|
26
26
|
|
|
27
|
+
easymd mcp Run the MCP server (stdio) so AI agents can edit your docs
|
|
28
|
+
easymd mcp-install [agent] Register the MCP server with Cursor / Claude (agent: --cursor | --claude-desktop)
|
|
29
|
+
|
|
27
30
|
easymd open <file> Open a local .md for real-time collaborative editing in the browser
|
|
28
31
|
easymd open <file> --port N Use a fixed port (default: random)
|
|
29
32
|
|
|
@@ -65,6 +68,17 @@ async function cmdOpen(args) {
|
|
|
65
68
|
console.log(`Note: ${absPath} does not exist yet — it will be created on save.`);
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
// The local browser editor pulls in yjs/y-websocket/express — loaded lazily and as
|
|
72
|
+
// optional deps so the core commands (login/auto/sync/mcp) never depend on them.
|
|
73
|
+
let startServer;
|
|
74
|
+
try {
|
|
75
|
+
({ startServer } = await import('../src/server.js'));
|
|
76
|
+
} catch {
|
|
77
|
+
console.error('`easymd open` needs the local editor packages. Install them with:');
|
|
78
|
+
console.error(' npm i -g easymd-cli (ensures optional deps), or run from the repo.');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
const { url, shutdown } = await startServer(absPath, { port });
|
|
69
83
|
console.log('');
|
|
70
84
|
console.log(' easymd');
|
|
@@ -144,6 +158,12 @@ async function main() {
|
|
|
144
158
|
return cmdWatch(rest);
|
|
145
159
|
case 'sync':
|
|
146
160
|
return cmdSync(rest);
|
|
161
|
+
case 'mcp':
|
|
162
|
+
// Importing starts the stdio MCP server (top-level await connect).
|
|
163
|
+
await import('../src/cli/mcp.mjs');
|
|
164
|
+
return;
|
|
165
|
+
case 'mcp-install':
|
|
166
|
+
return mcpInstall(rest[0]);
|
|
147
167
|
case 'open':
|
|
148
168
|
return cmdOpen(rest);
|
|
149
169
|
default:
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easymd-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Google Docs for markdown — collaborate on the actual .md file in your repo, live with humans and AI agents. CLI: login, auto-sync, and open .md files for real-time editing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"easymd": "./bin/easymd.js"
|
|
7
|
+
"easymd": "./bin/easymd.js",
|
|
8
|
+
"easymd-cli": "./bin/easymd.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"bin",
|
|
@@ -31,9 +32,13 @@
|
|
|
31
32
|
],
|
|
32
33
|
"license": "MIT",
|
|
33
34
|
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
34
36
|
"chokidar": "^4.0.3",
|
|
35
|
-
"express": "^4.21.2",
|
|
36
37
|
"open": "^10.1.0",
|
|
38
|
+
"zod": "^4.4.3"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"express": "^4.21.2",
|
|
37
42
|
"ws": "^8.18.0",
|
|
38
43
|
"y-websocket": "^2.1.0",
|
|
39
44
|
"yjs": "^13.6.24"
|
package/src/cli/auth.js
CHANGED
|
@@ -73,8 +73,17 @@ export async function login() {
|
|
|
73
73
|
export async function logout() {
|
|
74
74
|
await autoOff().catch(() => {});
|
|
75
75
|
await clearAuto().catch(() => {});
|
|
76
|
+
// Revoke the token server-side so it can't be reused even if the file leaked.
|
|
77
|
+
const creds = await getCredentials();
|
|
78
|
+
if (creds?.token && creds?.url) {
|
|
79
|
+
try {
|
|
80
|
+
await fetch(`${creds.url}/api/cli/revoke`, { method: 'POST', headers: { Authorization: `Bearer ${creds.token}` } });
|
|
81
|
+
} catch {
|
|
82
|
+
/* offline — local credentials are still removed below */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
76
85
|
await clearCredentials();
|
|
77
|
-
console.log('✓ Logged out.
|
|
86
|
+
console.log('✓ Logged out. Token revoked and credentials removed.');
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
export async function whoami() {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
// Where each agent reads its MCP server config (macOS paths).
|
|
6
|
+
const TARGETS = {
|
|
7
|
+
cursor: join(homedir(), '.cursor', 'mcp.json'),
|
|
8
|
+
'claude-desktop': join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// The bundled MCP server authenticates with the token from `easymd login`, so the
|
|
12
|
+
// config needs no secrets — just the command.
|
|
13
|
+
const SERVER = { command: 'easymd', args: ['mcp'] };
|
|
14
|
+
|
|
15
|
+
async function mergeConfig(file) {
|
|
16
|
+
let cfg = {};
|
|
17
|
+
try {
|
|
18
|
+
cfg = JSON.parse(await readFile(file, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
/* new file */
|
|
21
|
+
}
|
|
22
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
23
|
+
cfg.mcpServers.easymd = SERVER;
|
|
24
|
+
await mkdir(dirname(file), { recursive: true });
|
|
25
|
+
await writeFile(file, JSON.stringify(cfg, null, 2));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function mcpInstall(arg) {
|
|
29
|
+
const which = (arg || '').replace(/^--/, '');
|
|
30
|
+
const entries = which && TARGETS[which] ? [[which, TARGETS[which]]] : Object.entries(TARGETS);
|
|
31
|
+
|
|
32
|
+
console.log('Registering the easymd MCP server…\n');
|
|
33
|
+
for (const [name, file] of entries) {
|
|
34
|
+
try {
|
|
35
|
+
await mergeConfig(file);
|
|
36
|
+
console.log(` ✓ ${name.padEnd(15)} ${file}`);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.log(` ✗ ${name.padEnd(15)} ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log('\nFor Claude Code (CLI), run instead:');
|
|
42
|
+
console.log(' claude mcp add easymd -- easymd mcp\n');
|
|
43
|
+
console.log('Prerequisites:');
|
|
44
|
+
console.log(' • Install globally: npm i -g easymd-cli');
|
|
45
|
+
console.log(' • Log in once: easymd login');
|
|
46
|
+
console.log(' • Restart your agent (Cursor / Claude) to load it.\n');
|
|
47
|
+
console.log('The server authenticates with your saved login — no API keys to paste.');
|
|
48
|
+
}
|
package/src/cli/mcp.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// easymd MCP server (bundled in the CLI).
|
|
2
|
+
//
|
|
3
|
+
// Unlike the server-side MCP, this one is safe to run on any machine: it carries NO
|
|
4
|
+
// server secrets. It authenticates as YOU using the CLI token saved at `easymd login`
|
|
5
|
+
// (~/.easymd/credentials.json) and talks to the easymd web app's token-authed API.
|
|
6
|
+
// So any agent (Claude, Cursor, …) can read/create/edit the same live documents you do.
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { getCredentials } from './config.js';
|
|
11
|
+
|
|
12
|
+
const creds = await getCredentials();
|
|
13
|
+
const BASE = (creds?.url || process.env.EASYMD_URL || 'http://localhost:3000').replace(/\/$/, '');
|
|
14
|
+
const TOKEN = creds?.token || '';
|
|
15
|
+
|
|
16
|
+
const ok = (text) => ({ content: [{ type: 'text', text }] });
|
|
17
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
18
|
+
const stripOwner = (name) => (name.includes('__') ? name.split('__').slice(1).join('__') : name);
|
|
19
|
+
|
|
20
|
+
async function api(path, opts = {}) {
|
|
21
|
+
if (!TOKEN) throw new Error('Not logged in. Run `easymd login` first.');
|
|
22
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
23
|
+
...opts,
|
|
24
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${TOKEN}`, ...(opts.headers || {}) },
|
|
25
|
+
});
|
|
26
|
+
const json = await res.json().catch(() => ({}));
|
|
27
|
+
if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`);
|
|
28
|
+
return json;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const server = new McpServer({ name: 'easymd', version: '0.1.1' });
|
|
32
|
+
|
|
33
|
+
server.registerTool(
|
|
34
|
+
'list_documents',
|
|
35
|
+
{
|
|
36
|
+
title: 'List documents',
|
|
37
|
+
description: 'List all of your easymd documents (name, title, last updated). These are the live docs you edit in the browser.',
|
|
38
|
+
inputSchema: {},
|
|
39
|
+
},
|
|
40
|
+
async () => {
|
|
41
|
+
try {
|
|
42
|
+
const { documents = [] } = await api('/api/cli/documents');
|
|
43
|
+
if (!documents.length) return ok('No documents yet. Use create_document to add one.');
|
|
44
|
+
return ok(documents.map((d) => `- ${stripOwner(d.name)}${d.title ? ` — ${d.title}` : ''} (updated ${d.updated_at})`).join('\n'));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return fail(e.message);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
server.registerTool(
|
|
52
|
+
'read_document',
|
|
53
|
+
{
|
|
54
|
+
title: 'Read document',
|
|
55
|
+
description: 'Return the current markdown content of one of your documents by name.',
|
|
56
|
+
inputSchema: { name: z.string().describe('Document name, e.g. "product-spec"') },
|
|
57
|
+
},
|
|
58
|
+
async ({ name }) => {
|
|
59
|
+
try {
|
|
60
|
+
const { content } = await api(`/api/cli/documents?name=${encodeURIComponent(name)}`);
|
|
61
|
+
return ok(content || '(empty document)');
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return fail(e.message);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.registerTool(
|
|
69
|
+
'create_document',
|
|
70
|
+
{
|
|
71
|
+
title: 'Create document',
|
|
72
|
+
description: 'Create a new document in your account. It appears live in the editor and in your dashboard.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
name: z.string().describe('Short name/slug, e.g. "product-spec"'),
|
|
75
|
+
content: z.string().optional().describe('Optional initial markdown content'),
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
async ({ name, content }) => {
|
|
79
|
+
try {
|
|
80
|
+
const body = content && content.trim() ? content : `# ${name}\n\nCreated by an AI agent via MCP.\n`;
|
|
81
|
+
await api('/api/cli/documents', { method: 'POST', body: JSON.stringify({ name, title: name, content: body }) });
|
|
82
|
+
return ok(`Created "${name}". It now appears in your easymd dashboard and live editor.`);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return fail(e.message);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
server.registerTool(
|
|
90
|
+
'update_document',
|
|
91
|
+
{
|
|
92
|
+
title: 'Update document (replace)',
|
|
93
|
+
description: 'Replace the entire markdown content of a document. Syncs live to anyone with it open.',
|
|
94
|
+
inputSchema: { name: z.string(), content: z.string().describe('New full markdown content') },
|
|
95
|
+
},
|
|
96
|
+
async ({ name, content }) => {
|
|
97
|
+
try {
|
|
98
|
+
await api('/api/cli/documents', { method: 'POST', body: JSON.stringify({ name, content }) });
|
|
99
|
+
return ok(`Updated "${name}" (${content.length} chars). Live editors saw it instantly.`);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return fail(e.message);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
server.registerTool(
|
|
107
|
+
'append_to_document',
|
|
108
|
+
{
|
|
109
|
+
title: 'Append to document',
|
|
110
|
+
description: 'Append markdown to the end of a document without touching existing content. Syncs live.',
|
|
111
|
+
inputSchema: { name: z.string(), text: z.string().describe('Markdown to append') },
|
|
112
|
+
},
|
|
113
|
+
async ({ name, text }) => {
|
|
114
|
+
try {
|
|
115
|
+
const { bytes } = await api('/api/cli/documents', { method: 'POST', body: JSON.stringify({ name, op: 'append', text }) });
|
|
116
|
+
return ok(`Appended ${text.length} chars to "${name}" (now ${bytes} chars).`);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return fail(e.message);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const transport = new StdioServerTransport();
|
|
124
|
+
await server.connect(transport);
|
|
125
|
+
console.error(`easymd MCP ready → ${BASE} ${TOKEN ? '(authenticated)' : '(NOT logged in — run `easymd login`)'}`);
|