easymd-cli 0.1.0 → 0.1.2
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 +28 -1
- package/package.json +8 -3
- package/src/cli/auth.js +10 -1
- package/src/cli/hook-install.js +64 -0
- package/src/cli/hooks/easymd-stop-sync.sh +56 -0
- 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,12 @@
|
|
|
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
|
+
import { hookInstall, hookUninstall } from '../src/cli/hook-install.js';
|
|
11
12
|
|
|
12
13
|
const HELP = `
|
|
13
14
|
easymd — collaborate on markdown files in your repo, live with humans and AI agents
|
|
@@ -24,6 +25,11 @@ Usage:
|
|
|
24
25
|
easymd sync [dir] One-shot: push all .md files under dir (default: .) to your account
|
|
25
26
|
easymd watch [dir] Foreground watcher (what 'auto on' runs in the background)
|
|
26
27
|
|
|
28
|
+
easymd mcp Run the MCP server (stdio) so AI agents can edit your docs
|
|
29
|
+
easymd mcp-install [agent] Register the MCP server with Cursor / Claude (agent: --cursor | --claude-desktop)
|
|
30
|
+
easymd hook-install Auto-sync .md to your account after every Claude Code session (everywhere)
|
|
31
|
+
easymd hook-uninstall Remove the auto-sync hook
|
|
32
|
+
|
|
27
33
|
easymd open <file> Open a local .md for real-time collaborative editing in the browser
|
|
28
34
|
easymd open <file> --port N Use a fixed port (default: random)
|
|
29
35
|
|
|
@@ -65,6 +71,17 @@ async function cmdOpen(args) {
|
|
|
65
71
|
console.log(`Note: ${absPath} does not exist yet — it will be created on save.`);
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
// The local browser editor pulls in yjs/y-websocket/express — loaded lazily and as
|
|
75
|
+
// optional deps so the core commands (login/auto/sync/mcp) never depend on them.
|
|
76
|
+
let startServer;
|
|
77
|
+
try {
|
|
78
|
+
({ startServer } = await import('../src/server.js'));
|
|
79
|
+
} catch {
|
|
80
|
+
console.error('`easymd open` needs the local editor packages. Install them with:');
|
|
81
|
+
console.error(' npm i -g easymd-cli (ensures optional deps), or run from the repo.');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
const { url, shutdown } = await startServer(absPath, { port });
|
|
69
86
|
console.log('');
|
|
70
87
|
console.log(' easymd');
|
|
@@ -144,6 +161,16 @@ async function main() {
|
|
|
144
161
|
return cmdWatch(rest);
|
|
145
162
|
case 'sync':
|
|
146
163
|
return cmdSync(rest);
|
|
164
|
+
case 'mcp':
|
|
165
|
+
// Importing starts the stdio MCP server (top-level await connect).
|
|
166
|
+
await import('../src/cli/mcp.mjs');
|
|
167
|
+
return;
|
|
168
|
+
case 'mcp-install':
|
|
169
|
+
return mcpInstall(rest[0]);
|
|
170
|
+
case 'hook-install':
|
|
171
|
+
return hookInstall();
|
|
172
|
+
case 'hook-uninstall':
|
|
173
|
+
return hookUninstall();
|
|
147
174
|
case 'open':
|
|
148
175
|
return cmdOpen(rest);
|
|
149
176
|
default:
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easymd-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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,64 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { mkdir, readFile, writeFile, copyFile } from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
7
|
+
const SETTINGS = join(CLAUDE_DIR, 'settings.json');
|
|
8
|
+
const HOOK_PATH = fileURLToPath(new URL('./hooks/easymd-stop-sync.sh', import.meta.url));
|
|
9
|
+
const HOOK_CMD = `bash ${HOOK_PATH}`;
|
|
10
|
+
const MARKER = 'easymd-stop-sync';
|
|
11
|
+
|
|
12
|
+
async function readSettings() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(await readFile(SETTINGS, 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function writeSettings(s) {
|
|
20
|
+
await mkdir(CLAUDE_DIR, { recursive: true });
|
|
21
|
+
// Back up first — never clobber an existing config silently.
|
|
22
|
+
try {
|
|
23
|
+
await copyFile(SETTINGS, `${SETTINGS}.easymd-bak`);
|
|
24
|
+
} catch {
|
|
25
|
+
/* no existing file */
|
|
26
|
+
}
|
|
27
|
+
await writeFile(SETTINGS, JSON.stringify(s, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Registers a Claude Code Stop hook so that after any session that edits .md files,
|
|
31
|
+
// they auto-sync to your easymd account — across every project, everywhere. Merges
|
|
32
|
+
// alongside any existing Stop hooks (e.g. the obsidian-wiki capture hook).
|
|
33
|
+
export async function hookInstall() {
|
|
34
|
+
const s = await readSettings();
|
|
35
|
+
s.hooks = s.hooks || {};
|
|
36
|
+
s.hooks.Stop = Array.isArray(s.hooks.Stop) ? s.hooks.Stop : [];
|
|
37
|
+
|
|
38
|
+
if (JSON.stringify(s.hooks.Stop).includes(MARKER)) {
|
|
39
|
+
console.log('✓ easymd auto-sync hook is already installed.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
s.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: HOOK_CMD }] });
|
|
43
|
+
await writeSettings(s);
|
|
44
|
+
console.log('✓ Installed the easymd auto-sync hook in ~/.claude/settings.json');
|
|
45
|
+
console.log(' After any Claude Code session that edits .md files, they sync to your account.');
|
|
46
|
+
console.log(' Requires `easymd login`. A backup was saved to settings.json.easymd-bak.');
|
|
47
|
+
console.log(' Remove anytime with `easymd hook-uninstall`.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function hookUninstall() {
|
|
51
|
+
const s = await readSettings();
|
|
52
|
+
if (!s.hooks?.Stop?.length) {
|
|
53
|
+
console.log('easymd auto-sync hook is not installed.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const before = s.hooks.Stop.length;
|
|
57
|
+
s.hooks.Stop = s.hooks.Stop.filter((e) => !JSON.stringify(e).includes(MARKER));
|
|
58
|
+
if (s.hooks.Stop.length === before) {
|
|
59
|
+
console.log('easymd auto-sync hook is not installed.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await writeSettings(s);
|
|
63
|
+
console.log('✓ Removed the easymd auto-sync hook.');
|
|
64
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# easymd auto-sync Stop hook (adapted from the obsidian-wiki Stop-capture pattern).
|
|
3
|
+
#
|
|
4
|
+
# Fires on the Claude Code Stop event. If the session edited any .md files and you're
|
|
5
|
+
# logged into easymd, it pushes those docs to your account — so your markdown stays
|
|
6
|
+
# updated in easymd after every agent session, everywhere, with no daemon running.
|
|
7
|
+
#
|
|
8
|
+
# Always exits 0 (silent): it runs the sync directly, it never nudges Claude.
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
INPUT=$(cat)
|
|
12
|
+
|
|
13
|
+
# Need credentials — no-op if not logged in.
|
|
14
|
+
[[ -f "$HOME/.easymd/credentials.json" ]] || exit 0
|
|
15
|
+
|
|
16
|
+
# Pull cwd + transcript path from the hook payload.
|
|
17
|
+
read -r CWD TRANSCRIPT < <(printf '%s' "$INPUT" | python3 -c "
|
|
18
|
+
import json, sys
|
|
19
|
+
d = json.load(sys.stdin)
|
|
20
|
+
print(d.get('cwd', ''), d.get('transcript_path', ''))
|
|
21
|
+
" 2>/dev/null || echo " ")
|
|
22
|
+
[[ -n "$TRANSCRIPT" && -f "$TRANSCRIPT" ]] || exit 0
|
|
23
|
+
|
|
24
|
+
# Did this session write/edit any .md files?
|
|
25
|
+
EDITED_MD=$(python3 - "$TRANSCRIPT" <<'PYEOF'
|
|
26
|
+
import json, sys
|
|
27
|
+
n = 0
|
|
28
|
+
for line in open(sys.argv[1]):
|
|
29
|
+
line = line.strip()
|
|
30
|
+
if not line:
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
e = json.loads(line)
|
|
34
|
+
except json.JSONDecodeError:
|
|
35
|
+
continue
|
|
36
|
+
m = e.get("message") or {}
|
|
37
|
+
if m.get("role") != "assistant":
|
|
38
|
+
continue
|
|
39
|
+
for b in m.get("content") or []:
|
|
40
|
+
if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("name") in ("Write", "Edit", "NotebookEdit"):
|
|
41
|
+
fp = (b.get("input") or {}).get("file_path", "")
|
|
42
|
+
if fp.endswith(".md"):
|
|
43
|
+
n += 1
|
|
44
|
+
print(n)
|
|
45
|
+
PYEOF
|
|
46
|
+
)
|
|
47
|
+
[[ "${EDITED_MD:-0}" -ge 1 ]] || exit 0
|
|
48
|
+
|
|
49
|
+
# Resolve the easymd CLI from this script's own package (no PATH dependency).
|
|
50
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
51
|
+
EASYMD_BIN="$SCRIPT_DIR/../../../bin/easymd.js"
|
|
52
|
+
[[ -f "$EASYMD_BIN" ]] || exit 0
|
|
53
|
+
|
|
54
|
+
cd "${CWD:-$PWD}" 2>/dev/null || exit 0
|
|
55
|
+
node "$EASYMD_BIN" sync . --quiet >/dev/null 2>&1 || true
|
|
56
|
+
exit 0
|
|
@@ -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`)'}`);
|