@velua-ai/memory-mcp 0.1.5

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/README.md +121 -0
  2. package/dist/index.js +239 -0
  3. package/package.json +26 -0
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @velua-ai/memory-mcp
2
+
3
+ **Shared memory & TODO for your AI agents.** Connect any MCP-compatible agent
4
+ (Claude Code, Cursor, Codex, and more) to a single, shared per-project TODO list
5
+ and knowledge base — so every agent, and every teammate, works from the same
6
+ plan and the same context.
7
+
8
+ Stop re-explaining the project to each new agent. One shared memory. One shared
9
+ backlog. Always in sync.
10
+
11
+ ---
12
+
13
+ ## Why
14
+
15
+ - **One source of truth** — every agent reads and writes the same TODO and memory
16
+ for the project, instead of each keeping its own scratchpad.
17
+ - **Team-aware** — memory is scoped to your organization, so your whole team's
18
+ agents collaborate on the same board.
19
+ - **Frictionless sign-in** — no tokens to copy. The agent asks you to approve
20
+ once in the browser, and you're in.
21
+ - **Works everywhere** — any MCP client. Same tools, same data, wherever you code.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install -g @velua-ai/memory-mcp
27
+ ```
28
+
29
+ ## Sign in (one click)
30
+
31
+ The first time an agent needs it, it calls the **`velua_login`** tool — a browser
32
+ tab opens, you approve once, done. Your session is saved locally in
33
+ `~/.velua/mcp.json`. No token copy-paste.
34
+
35
+ > For CI/headless use, set `VELUA_TOKEN` with a token issued from your Velua
36
+ > account.
37
+
38
+ ## Add it to your agent
39
+
40
+ Each agent registers MCP servers a little differently — pick yours.
41
+
42
+ **Claude Code** — one command, no file editing:
43
+ ```bash
44
+ claude mcp add velua-memory -s user -- npx -y @velua-ai/memory-mcp
45
+ ```
46
+
47
+ **Cursor** (`~/.cursor/mcp.json` for all projects, or `.cursor/mcp.json` in a repo):
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "velua-memory": { "command": "npx", "args": ["-y", "@velua-ai/memory-mcp"] }
52
+ }
53
+ }
54
+ ```
55
+
56
+ **Codex CLI** — TOML, not JSON (`~/.codex/config.toml`):
57
+ ```toml
58
+ [mcp_servers.velua-memory]
59
+ command = "npx"
60
+ args = ["-y", "@velua-ai/memory-mcp"]
61
+ ```
62
+
63
+ **VS Code** (`.vscode/mcp.json`) — note the top-level key is `servers`, not `mcpServers`:
64
+ ```json
65
+ {
66
+ "servers": {
67
+ "velua-memory": { "command": "npx", "args": ["-y", "@velua-ai/memory-mcp"] }
68
+ }
69
+ }
70
+ ```
71
+
72
+ > Using `npx` means you never run a separate install step — it fetches and caches
73
+ > the package on first launch. Prefer a fixed binary? `npm install -g
74
+ > @velua-ai/memory-mcp` and use `"command": "velua-memory-mcp"` instead.
75
+
76
+ ## How projects are identified
77
+
78
+ Each TODO/memory board is scoped to a **project**. The project is resolved, in order:
79
+
80
+ 1. **`VELUA_PROJECT`** if set — an explicit override.
81
+ 2. A **`.velua.json`** file in the folder (or any parent) — see below.
82
+ 3. Otherwise your **git remote** name (e.g. `github.com/acme/checkout` → `checkout`).
83
+ 4. Otherwise the **current folder** name.
84
+
85
+ Because it keys off the git remote, two clones of the same repo — on different
86
+ machines or paths — map to the **same** shared board, so your whole team lands in
87
+ sync automatically.
88
+
89
+ ### No git? Drop a `.velua.json` in the folder
90
+
91
+ When a folder isn't a git repo (or you just want to pin the name), create a
92
+ `.velua.json` in it — the server searches up from the working directory, like git:
93
+
94
+ ```json
95
+ {
96
+ "project": "cliente-acme",
97
+ "agent": "cursor"
98
+ }
99
+ ```
100
+
101
+ All fields optional: `project` (the board name), `agent` (label for who created
102
+ each item), `apiUrl` (advanced). An `.velua.example.json` is included to copy.
103
+
104
+ ### Options (all optional)
105
+ | Env | Default | What it does |
106
+ |---|---|---|
107
+ | `VELUA_PROJECT` | your git repo name (or folder) | which project the TODO/memory belongs to |
108
+ | `VELUA_TOKEN` | — | a ready token, skips the browser sign-in |
109
+ | `VELUA_AGENT` | `mcp` | a label for who created each item |
110
+
111
+ ## Tools
112
+
113
+ - **`velua_login`** — sign this device in (opens the browser).
114
+ - **`todo_list` / `todo_add` / `todo_update`** — the project's shared TODO list.
115
+ - **`memory_get` / `memory_set`** — the project's shared memory (key → content).
116
+
117
+ ---
118
+
119
+ Part of the **Velua** platform. Learn more at [velua.ai](https://velua.ai).
120
+
121
+ _Licensed under Apache-2.0._
package/dist/index.js ADDED
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @velua-ai/memory-mcp — shared memory & TODO for your AI agents.
4
+ *
5
+ * Connects any MCP-compatible agent (Claude Code, Cursor, Codex, …) to a single
6
+ * shared per-project TODO list and knowledge base, scoped to your organization,
7
+ * so every agent and teammate works from the same plan and context.
8
+ *
9
+ * Frictionless sign-in: the `velua_login` tool opens the browser, you approve
10
+ * once, and the session is saved to ~/.velua/mcp.json. For CI/headless use, set
11
+ * VELUA_TOKEN.
12
+ *
13
+ * Options (env, all optional):
14
+ * VELUA_PROJECT which project the TODO/memory belongs to (default: git repo name)
15
+ * VELUA_TOKEN a ready token — skips the browser sign-in
16
+ * VELUA_AGENT a label for who created each item (default: mcp)
17
+ */
18
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
21
+ import { createHash, randomBytes } from "node:crypto";
22
+ import { createServer } from "node:http";
23
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
24
+ import { homedir } from "node:os";
25
+ import { join, dirname } from "node:path";
26
+ import { execSync } from "node:child_process";
27
+ function findProjectConfig() {
28
+ let dir = process.cwd();
29
+ for (;;) {
30
+ const p = join(dir, ".velua.json");
31
+ if (existsSync(p)) {
32
+ try {
33
+ return JSON.parse(readFileSync(p, "utf8"));
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ }
39
+ const parent = dirname(dir);
40
+ if (parent === dir)
41
+ return {};
42
+ dir = parent;
43
+ }
44
+ }
45
+ const CONFIG = findProjectConfig();
46
+ const API_URL = (process.env.VELUA_API_URL || CONFIG.apiUrl || "https://platform.velua.ai").replace(/\/+$/, "");
47
+ const SSO_URL = (process.env.VELUA_SSO_URL || "https://sso.velua.ai").replace(/\/+$/, "");
48
+ const CLIENT_ID = process.env.VELUA_OAUTH_CLIENT_ID || "velua-mcp";
49
+ const TOKEN_PATH = join(homedir(), ".velua", "mcp.json");
50
+ function loadToken() {
51
+ if (process.env.VELUA_TOKEN)
52
+ return process.env.VELUA_TOKEN.trim();
53
+ try {
54
+ return JSON.parse(readFileSync(TOKEN_PATH, "utf8")).token;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ function saveToken(token) {
61
+ try {
62
+ mkdirSync(join(homedir(), ".velua"), { recursive: true });
63
+ }
64
+ catch { }
65
+ writeFileSync(TOKEN_PATH, JSON.stringify({ token }, null, 2), { mode: 0o600 });
66
+ }
67
+ /** Projeto: env → .velua.json → nome do repo git (basename do remote) → cwd. */
68
+ function resolveProject() {
69
+ if (process.env.VELUA_PROJECT)
70
+ return process.env.VELUA_PROJECT.trim();
71
+ if (CONFIG.project)
72
+ return CONFIG.project.trim();
73
+ try {
74
+ const remote = execSync("git config --get remote.origin.url", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
75
+ const m = remote.match(/([^/:]+?)(?:\.git)?$/);
76
+ if (m?.[1])
77
+ return m[1];
78
+ }
79
+ catch { }
80
+ return process.cwd().split(/[/\\]/).pop() || "default";
81
+ }
82
+ const AGENT = process.env.VELUA_AGENT || CONFIG.agent || "mcp";
83
+ const PROJECT = resolveProject();
84
+ async function api(path, init = {}) {
85
+ const token = loadToken();
86
+ if (!token)
87
+ throw new Error("NOT_AUTHENTICATED");
88
+ const res = await fetch(`${API_URL}${path}`, {
89
+ ...init,
90
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}`, ...(init.headers || {}) },
91
+ });
92
+ if (res.status === 401)
93
+ throw new Error("NOT_AUTHENTICATED");
94
+ const json = await res.json().catch(() => ({}));
95
+ if (!res.ok)
96
+ throw new Error(json?.error || `platform ${res.status}`);
97
+ return json;
98
+ }
99
+ // ── OAuth localhost (baixa fricção) ───────────────────────────────────────────
100
+ function b64url(buf) {
101
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
102
+ }
103
+ /** Página de sucesso do callback OAuth — branded Velua (auto-fecha se possível). */
104
+ const SUCCESS_HTML = `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8">
105
+ <meta name="viewport" content="width=device-width,initial-scale=1"><title>Velua conectado</title>
106
+ <style>
107
+ :root{color-scheme:dark light}
108
+ *{box-sizing:border-box}
109
+ body{margin:0;min-height:100vh;display:grid;place-items:center;background:#0f0f14;color:#e9e9ee;
110
+ font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
111
+ .card{max-width:400px;padding:40px 36px;text-align:center;border:1px solid #23232c;border-radius:18px;
112
+ background:#17171d;box-shadow:0 30px 70px -30px rgba(0,0,0,.7)}
113
+ .mark{width:56px;height:56px;margin:0 auto 20px;border-radius:50%;display:grid;place-items:center;
114
+ background:linear-gradient(135deg,#FF8A5B,#C24BE0 52%,#6457F6)}
115
+ .mark svg{width:28px;height:28px;stroke:#fff;stroke-width:3;fill:none;stroke-linecap:round;stroke-linejoin:round}
116
+ h1{margin:0 0 8px;font-size:19px;font-weight:700}
117
+ p{margin:0;color:#a0a0ad;font-size:14px}
118
+ .brand{margin-top:22px;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:#6b6b78}
119
+ </style></head><body>
120
+ <div class="card">
121
+ <div class="mark"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></div>
122
+ <h1>Conectado à Velua</h1>
123
+ <p>Pode fechar esta aba e voltar ao seu terminal.</p>
124
+ <div class="brand">Velua</div>
125
+ </div>
126
+ <script>setTimeout(function(){window.close()},1200)</script>
127
+ </body></html>`;
128
+ async function loginViaBrowser() {
129
+ const verifier = b64url(randomBytes(32));
130
+ const challenge = b64url(createHash("sha256").update(verifier).digest());
131
+ const state = b64url(randomBytes(16));
132
+ let resolve, reject;
133
+ const done = new Promise((res, rej) => { resolve = res; reject = rej; });
134
+ const server = createServer(async (req, res) => {
135
+ const u = new URL(req.url || "/", "http://127.0.0.1");
136
+ if (!u.pathname.startsWith("/callback")) {
137
+ res.writeHead(404).end();
138
+ return;
139
+ }
140
+ const code = u.searchParams.get("code");
141
+ if (u.searchParams.get("state") !== state || !code) {
142
+ res.writeHead(400).end("invalid callback");
143
+ return;
144
+ }
145
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(SUCCESS_HTML);
146
+ server.close();
147
+ try {
148
+ // usa o mesmo port/redirect_uri mandado ao SSO (server.address() já é null após close()).
149
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
150
+ const tk = await fetch(`${SSO_URL}/oauth/token`, {
151
+ method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" },
152
+ body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirectUri, client_id: CLIENT_ID, code_verifier: verifier }),
153
+ });
154
+ const j = (await tk.json());
155
+ if (!j.access_token)
156
+ throw new Error(j.error || "sem access_token");
157
+ saveToken(j.access_token);
158
+ resolve(j.access_token);
159
+ }
160
+ catch (e) {
161
+ reject(e);
162
+ }
163
+ });
164
+ await new Promise((r) => server.listen(0, "127.0.0.1", r));
165
+ const port = server.address().port;
166
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
167
+ const url = `${SSO_URL}/oauth/authorize?` + new URLSearchParams({
168
+ response_type: "code", client_id: CLIENT_ID, redirect_uri: redirectUri,
169
+ code_challenge: challenge, code_challenge_method: "S256",
170
+ scope: "openid profile", state,
171
+ }).toString();
172
+ setTimeout(() => { try {
173
+ server.close();
174
+ }
175
+ catch { } ; reject(new Error("login expirou (2min)")); }, 120_000);
176
+ return { url, done };
177
+ }
178
+ function tryOpen(url) {
179
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
180
+ try {
181
+ execSync(`${cmd} "${url}"`, { stdio: "ignore" });
182
+ }
183
+ catch { }
184
+ }
185
+ // ── MCP tools ─────────────────────────────────────────────────────────────────
186
+ const TOOLS = [
187
+ { name: "velua_login", description: "Authenticate this device with Velua (opens a browser to approve). Call this first if other tools return NOT_AUTHENTICATED.", inputSchema: { type: "object", properties: {} } },
188
+ { name: "todo_list", description: "List the shared project TODO items (visible to every agent working on this project).", inputSchema: { type: "object", properties: {} } },
189
+ { name: "todo_add", description: "Add a TODO item to the shared project list.", inputSchema: { type: "object", properties: { title: { type: "string" }, detail: { type: "string" } }, required: ["title"] } },
190
+ { name: "todo_update", description: "Update a TODO's status or text. status: open | in_progress | done.", inputSchema: { type: "object", properties: { id: { type: "string" }, status: { type: "string" }, title: { type: "string" }, detail: { type: "string" } }, required: ["id"] } },
191
+ { name: "memory_get", description: "Read shared project memory. Omit key to list all keys, or pass key for one.", inputSchema: { type: "object", properties: { key: { type: "string" } } } },
192
+ { name: "memory_set", description: "Write/update a shared project memory entry (key → content).", inputSchema: { type: "object", properties: { key: { type: "string" }, content: { type: "string" } }, required: ["key", "content"] } },
193
+ ];
194
+ const server = new Server({ name: "velua-memory", version: "0.1.0" }, { capabilities: { tools: {} } });
195
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
196
+ function text(t) { return { content: [{ type: "text", text: t }] }; }
197
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
198
+ const { name, arguments: args = {} } = req.params;
199
+ try {
200
+ if (name === "velua_login") {
201
+ const { url, done } = await loginViaBrowser();
202
+ tryOpen(url);
203
+ await done;
204
+ return text(`Autenticado com sucesso na Velua. Projeto: "${PROJECT}".`);
205
+ }
206
+ if (name === "todo_list") {
207
+ const d = await api(`/api/project/${encodeURIComponent(PROJECT)}/todos`);
208
+ const items = (d.todos || []).map((t) => `- [${t.status}] ${t.title}${t.detail ? " — " + t.detail : ""} (id: ${t.id}${t.createdByAgent ? ", por " + t.createdByAgent : ""})`).join("\n");
209
+ return text(items ? `TODO de "${PROJECT}":\n${items}` : `Nenhum TODO em "${PROJECT}".`);
210
+ }
211
+ if (name === "todo_add") {
212
+ const d = await api(`/api/project/${encodeURIComponent(PROJECT)}/todos`, { method: "POST", body: JSON.stringify({ title: args.title, detail: args.detail, agent: AGENT }) });
213
+ return text(`Adicionado: "${d.todo.title}" (id: ${d.todo.id}).`);
214
+ }
215
+ if (name === "todo_update") {
216
+ const d = await api(`/api/project/todos/${encodeURIComponent(args.id)}`, { method: "PATCH", body: JSON.stringify({ status: args.status, title: args.title, detail: args.detail }) });
217
+ return text(`Atualizado: "${d.todo.title}" → ${d.todo.status}.`);
218
+ }
219
+ if (name === "memory_get") {
220
+ const q = args.key ? `?key=${encodeURIComponent(args.key)}` : "";
221
+ const d = await api(`/api/project/${encodeURIComponent(PROJECT)}/memory${q}`);
222
+ const rows = (d.memory || []).map((m) => `## ${m.key}\n${m.content}`).join("\n\n");
223
+ return text(rows || `Nenhuma memória em "${PROJECT}".`);
224
+ }
225
+ if (name === "memory_set") {
226
+ await api(`/api/project/${encodeURIComponent(PROJECT)}/memory`, { method: "POST", body: JSON.stringify({ key: args.key, content: args.content }) });
227
+ return text(`Memória "${args.key}" salva em "${PROJECT}".`);
228
+ }
229
+ return text(`tool desconhecida: ${name}`);
230
+ }
231
+ catch (e) {
232
+ const msg = e.message;
233
+ if (msg === "NOT_AUTHENTICATED")
234
+ return text("Não autenticado. Chame a tool `velua_login` — vou abrir o navegador para você aprovar o acesso (uma vez só).");
235
+ return text(`Erro: ${msg}`);
236
+ }
237
+ });
238
+ await server.connect(new StdioServerTransport());
239
+ console.error(`[velua-memory-mcp] pronto — projeto "${PROJECT}", api ${API_URL}`);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@velua-ai/memory-mcp",
3
+ "version": "0.1.5",
4
+ "description": "Velua shared memory & TODO MCP server — connect any agent (Claude Code, Cursor, Codex) to a shared per-project TODO/memory.",
5
+ "type": "module",
6
+ "bin": {
7
+ "velua-memory-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/index.js",
15
+ "dev": "tsx src/index.ts"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "typescript": "^5.6.0",
23
+ "tsx": "^4.0.0"
24
+ },
25
+ "license": "Apache-2.0"
26
+ }