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 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 in `--server` mode (defaults to `127.0.0.1`, loopback-only). Set to `0.0.0.0` to expose on the LAN.
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 + HTTP)**
283
- Best for local integration (e.g., Claude Desktop, IDE extensions). The MCP client launches the process per session over stdio; an HTTP Stream transport is also opened on an auto-discovered port near `HTTP_STREAM_PORT`.
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
- </personalization_layer>
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 { findAvailablePort, isPortInUse } from '../utils/net.js';
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
- // In --server (daemon) mode we run HTTP-only: no client reads the stdio
310
- // transport, so we skip it. Otherwise behavior is unchanged (stdio + HTTP).
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
- mode: serverMode ? 'server' : 'stdio+http',
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
- if (serverMode) {
440
- // A shared daemon must own a stable URL — never silently drift to another
441
- // port. Fail loudly if the configured port is already taken on our bind
442
- // host (a listener on a different interface is not our conflict).
443
- if (await isPortInUse(desiredPort, HTTP_STREAM_HOST)) {
444
- throw new McpError(ErrorCode.InternalError, `--server: port ${desiredPort} (HTTP_STREAM_PORT) is already in use. ` +
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);
@@ -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) — matching `findAvailablePort`'s "globally free" semantics.
7
- * Pass a specific host (e.g. `127.0.0.1`) to check only that interface, so a
8
- * listener bound to a different interface (e.g. wildcard) doesn't register as
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
- * Finds an available port, starting from a given port and incrementing by a specified amount.
38
- * @param startPort The port to start checking from.
39
- * @param incrementBy The amount to increment the port by if it's in use. Defaults to 2.
40
- * @returns A promise that resolves to an available port number.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roam-research-mcp",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "MCP server and CLI for Roam Research",
5
5
  "private": false,
6
6
  "repository": {