roam-research-mcp 3.0.0 → 3.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/README.md +7 -5
- package/build/Roam_Markdown_Cheatsheet.md +179 -3
- package/build/server/roam-server.js +24 -21
- package/build/utils/net.js +8 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -273,14 +273,16 @@ So: to mark a graph as needing the write key, set `protected: true` on it and co
|
|
|
273
273
|
|
|
274
274
|
*Optional:*
|
|
275
275
|
- `ROAM_MEMORIES_TAG`: Default tag for `roam_remember`/`roam_recall` (fallback when per-graph `memoriesTag` not set).
|
|
276
|
-
- `HTTP_STREAM_PORT`: Port for the HTTP Stream transport (defaults to 8088).
|
|
277
|
-
- `HTTP_STREAM_HOST`: Host to bind the HTTP transport to
|
|
276
|
+
- `HTTP_STREAM_PORT`: Port for the HTTP Stream transport (defaults to 8088). **`--server` mode only** — stdio mode opens no socket, so this is ignored there.
|
|
277
|
+
- `HTTP_STREAM_HOST`: Host to bind the HTTP transport to (defaults to `127.0.0.1`, loopback-only). **`--server` mode only.** Set to `0.0.0.0` to expose on the LAN, and set `HTTP_AUTH_TOKEN` when you do.
|
|
278
278
|
- `HTTP_AUTH_TOKEN`: Optional bearer token that locks the **whole** HTTP endpoint. Unset = open (fine for loopback). When set, every MCP request must send `Authorization: Bearer <token>` (`GET /health` stays open). Use it whenever you bind beyond `127.0.0.1`. Different from `ROAM_SYSTEM_WRITE_KEY` — see [Two kinds of access control](#two-kinds-of-access-control-and-how-they-differ).
|
|
279
279
|
|
|
280
280
|
### Running the Server
|
|
281
281
|
|
|
282
|
-
**1. Default Mode (stdio
|
|
283
|
-
Best for local integration (e.g., Claude Desktop, IDE extensions). The MCP client launches the process per session
|
|
282
|
+
**1. Default Mode (stdio)**
|
|
283
|
+
Best for local integration (e.g., Claude Desktop, IDE extensions). The MCP client launches the process per session and talks to it over stdin/stdout. **No port is opened** — nothing about MCP over stdio needs one.
|
|
284
|
+
|
|
285
|
+
> Before 3.1.0 this mode *also* opened an HTTP listener, and bound it to every interface. If you were using that endpoint, run a `--server` daemon instead; see below.
|
|
284
286
|
|
|
285
287
|
```bash
|
|
286
288
|
npx roam-research-mcp
|
|
@@ -305,7 +307,7 @@ roam server stop # stop a CLI-started daemon
|
|
|
305
307
|
|
|
306
308
|
`roam server status` works no matter how the daemon was launched (it probes `/health`), so it also reports a daemon started by a LaunchAgent/systemd unit. State (pidfile + log) lives in `~/.roam/` (override with `ROAM_HOME`).
|
|
307
309
|
|
|
308
|
-
In `--server` mode the server:
|
|
310
|
+
The two modes are mutually exclusive, and each opens exactly one transport: stdio mode speaks stdio and binds nothing, `--server` speaks HTTP and reads no stdin. In `--server` mode the server:
|
|
309
311
|
- runs **HTTP-only** (no stdio transport),
|
|
310
312
|
- binds the **exact** `HTTP_STREAM_PORT` on `HTTP_STREAM_HOST` and **exits non-zero if the port is taken** (no silent drift — a shared daemon must keep a stable URL),
|
|
311
313
|
- exposes `GET /health` → `{"status":"ok", ...}` for liveness checks.
|
|
@@ -244,8 +244,6 @@ Server returns `{"uid_map": {"parent": "Xk7mN2pQ9"}}`.
|
|
|
244
244
|
**Open question:** `{{[[TODO]]}} Research: <question> #[[open questions]]`
|
|
245
245
|
|
|
246
246
|
---
|
|
247
|
-
<personalization_layer>
|
|
248
|
-
|
|
249
247
|
# Roam Preferences — Personalization Layer
|
|
250
248
|
|
|
251
249
|
> This section contains YOUR specific conventions, tagging philosophy, and graph-specific rules. Customize to match your workflow.
|
|
@@ -254,21 +252,199 @@ Server returns `{"uid_map": {"parent": "Xk7mN2pQ9"}}`.
|
|
|
254
252
|
|
|
255
253
|
## Graph-Level Behaviors
|
|
256
254
|
|
|
255
|
+
### On Creating New Pages
|
|
256
|
+
<!-- CUSTOMIZE: What should happen when a new page is created? -->
|
|
257
|
+
- After creating a new page, add a reference block on today's daily page: `Created page: [[New Page Name]]`
|
|
258
|
+
- <!-- Add any naming conventions, required metadata, etc. -->
|
|
259
|
+
|
|
260
|
+
### On Adding Content
|
|
261
|
+
<!-- CUSTOMIZE: Any rules about where/how content gets added? -->
|
|
262
|
+
- Default location for quick captures: Daily page
|
|
263
|
+
- Long-form content: Create dedicated page, link from daily page
|
|
264
|
+
- <!-- Your preferences here -->
|
|
265
|
+
|
|
266
|
+
---
|
|
257
267
|
|
|
258
268
|
## Tagging Philosophy
|
|
259
269
|
|
|
270
|
+
### Core Principle
|
|
271
|
+
> Tag for **intellectual collision** and **future discovery**, not just categorization. Every tag should maximize potential for unexpected connections.
|
|
272
|
+
|
|
273
|
+
### The Serendipity Test
|
|
274
|
+
Before tagging, ask: *"Could this concept surprise me by connecting to something completely unrelated?"*
|
|
275
|
+
|
|
276
|
+
### What To Tag — Decision Framework
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
ASK YOURSELF:
|
|
280
|
+
┌─ How will Future Me find this?
|
|
281
|
+
│ └─ Tag by retrieval context, not just content
|
|
282
|
+
│
|
|
283
|
+
├─ What domain does this belong to?
|
|
284
|
+
│ └─ Use broad category tags: #[[knowledge management]], #[[decision-making]]
|
|
285
|
+
│
|
|
286
|
+
├─ Is this a proper noun?
|
|
287
|
+
│ └─ YES → Wrap name (no titles): [[Werner Erhard]], [[NASA]]
|
|
288
|
+
│ └─ For abbreviations: [NASA]([[National Aeronautics and Space Administration (NASA)]])
|
|
289
|
+
│
|
|
290
|
+
├─ Could this alias to existing page?
|
|
291
|
+
│ └─ YES → [displayed phrase]([[existing page name]])
|
|
292
|
+
│ └─ Example: [frameworks for decisions]([[decision-making frameworks]])
|
|
293
|
+
│
|
|
294
|
+
└─ Parent block with children?
|
|
295
|
+
└─ Tag parent when category applies to all children
|
|
296
|
+
└─ Tag individual children for specific categorization
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Tag Type Selection
|
|
300
|
+
|
|
301
|
+
| Use This | When |
|
|
302
|
+
|----------|------|
|
|
303
|
+
| `[[Page Reference]]` | Concept deserves its own page, will be expanded |
|
|
304
|
+
| `#[[hashtag]]` | Categorization, filtering, won't be a standalone page |
|
|
305
|
+
| `#single-word` | Simple, unambiguous category |
|
|
306
|
+
| Attribute `Type::` | Structured metadata for queries |
|
|
307
|
+
|
|
308
|
+
### WHEN creating Endnotes/Footnotes:
|
|
309
|
+
- Find/Create the block with heading "Footnotes::" and nest footnote item below. (Footnotes do not need to be on the same page as the block to which it references. Typically on the same page unless instructed otherwise.)
|
|
310
|
+
- If not known, retrieve the block_uid reference for this footnote item.
|
|
311
|
+
- In the block referencing the footnote, append the reference with footnote-item-block_id, example: "- <block_text> #ref ((block_uid))"
|
|
312
|
+
|
|
313
|
+
### Structural Tagging (Beyond Content)
|
|
314
|
+
|
|
315
|
+
Tag by **patterns and mechanisms**, not just subjects:
|
|
316
|
+
|
|
317
|
+
| Structural Tag | Connects |
|
|
318
|
+
|----------------|----------|
|
|
319
|
+
| `#[[has feedback loops]]` | Systems, habits, markets, conversations |
|
|
320
|
+
| `#[[requires calibration]]` | Instruments, relationships, AI prompts |
|
|
321
|
+
| `#[[exhibits emergence]]` | Complexity, culture, creativity |
|
|
322
|
+
| `#[[perspective switching]]` | Photography, negotiation, analysis |
|
|
323
|
+
| `#[[flow dynamics]]` | Fluids, music, conversation, sequences |
|
|
324
|
+
|
|
325
|
+
### Problem-Oriented Tagging
|
|
326
|
+
|
|
327
|
+
Tag by problems solved, not methods used:
|
|
328
|
+
|
|
329
|
+
- `#[[breaking cognitive constraints]]`
|
|
330
|
+
- `#[[expanding solution spaces]]`
|
|
331
|
+
- `#[[preventing expert blindness]]`
|
|
332
|
+
|
|
333
|
+
### Temporal & State-Based Tags
|
|
334
|
+
|
|
335
|
+
| Tag Type | Examples |
|
|
336
|
+
|----------|----------|
|
|
337
|
+
| Future relevance | `#[[will be relevant in 5 years]]`, `#[[connects to unborn projects]]` |
|
|
338
|
+
| Mental state triggers | `#[[feeling stuck in patterns]]`, `#[[needing fresh perspective]]` |
|
|
339
|
+
| Review scheduling | `[[For review]]: [[August 12th, 2026]]` |
|
|
340
|
+
|
|
341
|
+
---
|
|
260
342
|
|
|
261
343
|
## Formatting Conventions
|
|
262
344
|
|
|
345
|
+
### Quotes
|
|
346
|
+
```
|
|
347
|
+
<quote text> —[[Author Name]] #quote #[[topic1]] #[[topic2]]
|
|
348
|
+
```
|
|
349
|
+
Always include 2-3 relevant hashtags after quotes.
|
|
350
|
+
|
|
351
|
+
### TODOs and Follow-ups
|
|
352
|
+
```
|
|
353
|
+
{{[[TODO]]}} <action needed>
|
|
354
|
+
{{[[TODO]]}} #researchThis : <topic to investigate>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Scheduled Reviews
|
|
358
|
+
|
|
359
|
+
- Any block tagged with a date will show on that respective daily page.
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
[[For review]]: [[Date in ordinal format]]
|
|
363
|
+
```
|
|
364
|
+
Optional labels: "Deadline", "Approved", "Pending", "Deferred", "Postponed until"
|
|
365
|
+
|
|
366
|
+
### Aliasing for Case Sensitivity
|
|
367
|
+
When a tag would awkwardly affect sentence capitalization:
|
|
368
|
+
```
|
|
369
|
+
[Cognitive biases]([[cognitive biases]]) affect decision-making...
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Definitions (OVERRIDE)
|
|
373
|
+
```
|
|
374
|
+
#def [[<term>]] : <definition>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
263
378
|
|
|
264
379
|
## Constraints & Guardrails
|
|
265
380
|
|
|
381
|
+
### DON'T
|
|
382
|
+
- **Overtag** — Quality over quantity; each tag should earn its place
|
|
383
|
+
- **Tag obvious/redundant** — If parent block is tagged, children inherit context
|
|
384
|
+
- **Use inconsistent capitalization** — Tags are lowercase unless proper nouns
|
|
385
|
+
- **Create orphan tags** — Check if existing page/tag serves the purpose
|
|
386
|
+
- **Bold Attributes** - ❌ `**Attribute**::`, ✅ `Attribute::` (Roam auto-formats)
|
|
387
|
+
- **Separators** - `---` Don't use them.
|
|
388
|
+
|
|
389
|
+
### DO
|
|
390
|
+
- **Think retrieval-first** — How will you search for this later?
|
|
391
|
+
- **Cross-pollinate domains** — Force unlikely intellectual meetings
|
|
392
|
+
- **Update aging tags** — As interests evolve, so should tag vocabulary
|
|
393
|
+
- **Track surprise discoveries** — When unexpected connections yield insights, engineer more of those patterns
|
|
394
|
+
|
|
395
|
+
---
|
|
266
396
|
|
|
267
397
|
## Custom Rules
|
|
268
398
|
|
|
399
|
+
<!--
|
|
400
|
+
CUSTOMIZE THIS SECTION with your specific conventions:
|
|
401
|
+
- Naming patterns for certain page types
|
|
402
|
+
- Required attributes for books/articles/people
|
|
403
|
+
- Project-specific tagging schemes
|
|
404
|
+
- Integration rules with other tools
|
|
405
|
+
- etc.
|
|
406
|
+
-->
|
|
407
|
+
|
|
408
|
+
### Example Custom Rules (modify as needed):
|
|
409
|
+
|
|
410
|
+
**Books:**
|
|
411
|
+
```
|
|
412
|
+
[[Book/<title> | <author>]]
|
|
413
|
+
Type:: Book
|
|
414
|
+
Author:: [[Author Name]]
|
|
415
|
+
Status:: Reading | Completed | Abandoned
|
|
416
|
+
Rating:: X/5
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**People:**
|
|
420
|
+
```
|
|
421
|
+
[[Person Name]]
|
|
422
|
+
Type:: Person
|
|
423
|
+
Context:: How I know them
|
|
424
|
+
```
|
|
425
|
+
- When linking bibliographic references —>
|
|
426
|
+
Example: `McAdams, D.P. (2001) [The Psychology of Life Stories](https://journals.sagepub.com/doi/10.1037/1089-2680.5.2.100) — foundational paper`
|
|
427
|
+
- [McAdams, D.P.]([[Dan McAdams]]) - author's name in the graph
|
|
428
|
+
- If source URL, link to source: [The Psychology of Life Stories](https://journals.sagepub.com/doi/10.1037/1089-2680.5.2.100)
|
|
429
|
+
- If notes page exists or will exist in Roam: append ` | [Notes]([[Article/The Psychology of Life Stories]]), if not, just leave it without link.
|
|
430
|
+
|
|
431
|
+
**Projects:**
|
|
432
|
+
```
|
|
433
|
+
[[Project/<project anme>]]
|
|
434
|
+
Status:: Active | Paused | Completed
|
|
435
|
+
Start:: [[Date]]
|
|
436
|
+
```
|
|
437
|
+
---
|
|
269
438
|
|
|
270
439
|
## Integration Notes
|
|
271
440
|
|
|
272
441
|
<!-- CUSTOMIZE: Any rules about how Roam integrates with your other tools/systems -->
|
|
273
442
|
|
|
274
|
-
|
|
443
|
+
- Daily pages serve as: <!-- inbox / journal / task list / etc. -->
|
|
444
|
+
- Weekly reviews occur on: <!-- day of week -->
|
|
445
|
+
- Content flows from: <!-- capture tools, read-later apps, etc. -->
|
|
446
|
+
- Content flows to: <!-- publishing, archives, etc. -->
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
*End of Personalization Layer*
|
|
@@ -12,7 +12,7 @@ import { readFileSync } from 'node:fs';
|
|
|
12
12
|
import { join, dirname } from 'node:path';
|
|
13
13
|
import { createServer } from 'node:http';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import {
|
|
15
|
+
import { isPortInUse } from '../utils/net.js';
|
|
16
16
|
import { CORS_ORIGINS } from '../config/environment.js';
|
|
17
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
18
|
const __dirname = dirname(__filename);
|
|
@@ -306,12 +306,18 @@ export class RoamServer {
|
|
|
306
306
|
async run(options = {}) {
|
|
307
307
|
const { serverMode = false } = options;
|
|
308
308
|
try {
|
|
309
|
-
//
|
|
310
|
-
//
|
|
309
|
+
// The two transports are mutually exclusive, and each mode opens exactly
|
|
310
|
+
// one. Stdio mode talks to the client that spawned it over stdin/stdout
|
|
311
|
+
// and returns here — it opens no socket at all. Nothing about MCP over
|
|
312
|
+
// stdio needs one, and a listener nobody asked for is pure attack
|
|
313
|
+
// surface: until 3.1.0 stdio mode also bound an HTTP port, which shipped
|
|
314
|
+
// a token-free MCP endpoint per spawned instance. Run `--server` when
|
|
315
|
+
// you want HTTP; that is what it is for.
|
|
311
316
|
if (!serverMode) {
|
|
312
317
|
const stdioMcpServer = this.createMcpServer();
|
|
313
318
|
const stdioTransport = new StdioServerTransport();
|
|
314
319
|
await stdioMcpServer.connect(stdioTransport);
|
|
320
|
+
return;
|
|
315
321
|
}
|
|
316
322
|
// Track active transports by session ID for proper session management
|
|
317
323
|
const activeSessions = new Map();
|
|
@@ -343,7 +349,11 @@ export class RoamServer {
|
|
|
343
349
|
status: 'ok',
|
|
344
350
|
name: 'roam-research-mcp',
|
|
345
351
|
version: serverVersion,
|
|
346
|
-
|
|
352
|
+
// Always 'server' since 3.1.0: this handler is only reachable in
|
|
353
|
+
// --server mode. Kept as a field because clients read it, and the
|
|
354
|
+
// retired 'stdio+http' value is how they can tell they are talking
|
|
355
|
+
// to an older build that still had the stdio-mode listener.
|
|
356
|
+
mode: 'server',
|
|
347
357
|
auth: HTTP_AUTH_TOKEN ? 'required' : 'none',
|
|
348
358
|
graphs: this.registry.getAvailableGraphs(),
|
|
349
359
|
defaultGraph: this.registry.defaultKey,
|
|
@@ -436,24 +446,17 @@ export class RoamServer {
|
|
|
436
446
|
}
|
|
437
447
|
});
|
|
438
448
|
const desiredPort = parseInt(HTTP_STREAM_PORT);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
`Stop the process using it, or set HTTP_STREAM_PORT to a free port.`);
|
|
446
|
-
}
|
|
447
|
-
httpServer.listen(desiredPort, HTTP_STREAM_HOST, () => {
|
|
448
|
-
console.error(`roam-research-mcp v${serverVersion} (--server) listening on ` +
|
|
449
|
-
`http://${HTTP_STREAM_HOST}:${desiredPort}/ (health: /health)`);
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
const availableHttpPort = await findAvailablePort(desiredPort);
|
|
454
|
-
httpServer.listen(availableHttpPort, () => {
|
|
455
|
-
});
|
|
449
|
+
// A shared daemon must own a stable URL — never silently drift to another
|
|
450
|
+
// port. Fail loudly if the configured port is already taken on our bind
|
|
451
|
+
// host (a listener on a different interface is not our conflict).
|
|
452
|
+
if (await isPortInUse(desiredPort, HTTP_STREAM_HOST)) {
|
|
453
|
+
throw new McpError(ErrorCode.InternalError, `--server: port ${desiredPort} (HTTP_STREAM_PORT) is already in use. ` +
|
|
454
|
+
`Stop the process using it, or set HTTP_STREAM_PORT to a free port.`);
|
|
456
455
|
}
|
|
456
|
+
httpServer.listen(desiredPort, HTTP_STREAM_HOST, () => {
|
|
457
|
+
console.error(`roam-research-mcp v${serverVersion} (--server) listening on ` +
|
|
458
|
+
`http://${HTTP_STREAM_HOST}:${desiredPort}/ (health: /health)`);
|
|
459
|
+
});
|
|
457
460
|
}
|
|
458
461
|
catch (error) {
|
|
459
462
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
package/build/utils/net.js
CHANGED
|
@@ -3,10 +3,9 @@ import { createServer } from 'node:net';
|
|
|
3
3
|
* Checks if a given port is currently in use.
|
|
4
4
|
* @param port The port to check.
|
|
5
5
|
* @param host Optional host to probe. When omitted, probes the wildcard address
|
|
6
|
-
* (any interface) —
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* a conflict for a host-specific bind.
|
|
6
|
+
* (any interface). Pass the host you intend to bind — a wildcard probe and a
|
|
7
|
+
* host-specific bind disagree in both directions, so the two must match or
|
|
8
|
+
* the answer is about a port nobody is going to open.
|
|
10
9
|
* @returns A promise that resolves to true if the port is in use, and false otherwise.
|
|
11
10
|
*/
|
|
12
11
|
export function isPortInUse(port, host) {
|
|
@@ -33,16 +32,8 @@ export function isPortInUse(port, host) {
|
|
|
33
32
|
}
|
|
34
33
|
});
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
export async function findAvailablePort(startPort, incrementBy = 2) {
|
|
43
|
-
let port = startPort;
|
|
44
|
-
while (await isPortInUse(port)) {
|
|
45
|
-
port += incrementBy;
|
|
46
|
-
}
|
|
47
|
-
return port;
|
|
48
|
-
}
|
|
35
|
+
// `findAvailablePort` lived here until 3.1.0. It existed so stdio mode could
|
|
36
|
+
// drift to a free port when 8088 was taken, and it has no caller now that stdio
|
|
37
|
+
// opens no socket: `--server` binds the exact configured port and fails loudly
|
|
38
|
+
// rather than drifting, because a shared daemon must keep a stable URL.
|
|
39
|
+
// Anything reaching for it again probably wants that failure instead.
|