@andespindola/brainlink 0.1.0-alpha.1 → 0.1.0-alpha.10

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/AGENTS.md CHANGED
@@ -27,12 +27,16 @@ By default, the installed Brainlink CLI uses `$HOME/.brainlink/vault` as its vau
27
27
  Use this loop when using Brainlink as memory:
28
28
 
29
29
  1. Write durable knowledge into Markdown notes.
30
- 2. Link related notes with `[[Note Title]]`.
30
+ 2. Link related notes with explicit `[[Note Title]]` wiki links inside the note body.
31
31
  3. Add explicit `#tags` for retrieval.
32
32
  4. Run `index` after writes.
33
33
  5. Run `context "<task or question>"` before answering.
34
34
  6. Use the returned sources as grounded context.
35
35
 
36
+ `context` is read-only. It does not create notes, backlinks, graph edges or durable memory by itself. A relationship exists only when a Markdown note contains a `[[wiki link]]` to another note and the vault has been indexed after that write.
37
+
38
+ When an agent adds durable memory, it should connect the new note to at least one existing concept unless the note is intentionally a root concept. Prefer exact note titles in links, for example `[[Architecture]]`, and run `broken-links`, `orphans` or `validate` when the graph looks disconnected.
39
+
36
40
  ## Commands
37
41
 
38
42
  ```bash
@@ -82,7 +86,7 @@ npm run dev -- watch --vault ./vault
82
86
  Start MCP over stdio:
83
87
 
84
88
  ```bash
85
- npm run dev -- mcp
89
+ npm run dev:mcp
86
90
  ```
87
91
 
88
92
  Automation-facing CLI commands support `--json`. When invoking through `npm`, use `npm run --silent dev -- ...` so stdout remains valid JSON.
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="./assets/brainlink-logo.svg" alt="Brainlink" width="720">
3
+ </p>
4
+
1
5
  # Brainlink
2
6
 
3
7
  Local-first memory and knowledge graph for AI agents.
@@ -64,7 +68,7 @@ Markdown is the source of truth. `.brainlink/brainlink.db` is only a rebuildable
64
68
  - Agent namespaces under `agents/<agent-id>/`.
65
69
  - CLI with machine-readable `--json` output.
66
70
  - Short CLI alias: `blink`.
67
- - Compatible with MCP servers that execute local CLI commands.
71
+ - Built-in MCP stdio server for agent tool integration.
68
72
  - Local HTTP API.
69
73
  - Realtime graph UI with agent selector and colored knowledge groups.
70
74
 
@@ -76,7 +80,7 @@ Requirements:
76
80
  - npm
77
81
 
78
82
  ```bash
79
- npm install -g @andespindola/brainlink
83
+ npm install -g @andespindola/brainlink@latest
80
84
  ```
81
85
 
82
86
  The package installs two equivalent commands:
@@ -88,6 +92,14 @@ blink --help
88
92
 
89
93
  Use `brainlink` when clarity matters. Use `blink` for faster daily terminal usage.
90
94
 
95
+ The npm package page may show `npm i @andespindola/brainlink`. That installs
96
+ Brainlink as a project dependency. Use `-g` when you want the terminal commands
97
+ available globally, or run it without a global install:
98
+
99
+ ```bash
100
+ npx @andespindola/brainlink@latest --help
101
+ ```
102
+
91
103
  ## Agent Autonomous Setup
92
104
 
93
105
  This section is written for agents. If you are an AI agent operating in a user workspace, use this workflow to install and use Brainlink without requiring the user to run commands manually.
@@ -101,7 +113,7 @@ command -v blink || command -v brainlink
101
113
  If neither command exists, install it:
102
114
 
103
115
  ```bash
104
- npm install -g @andespindola/brainlink
116
+ npm install -g @andespindola/brainlink@latest
105
117
  ```
106
118
 
107
119
  Then verify:
@@ -177,6 +189,15 @@ blink add "Testing Policy" \
177
189
  --content "Run npm run check before final delivery. Related: [[Release Checklist]]. #testing #process"
178
190
  ```
179
191
 
192
+ Brainlink does not infer durable graph relationships from generated context. A context result is only a read package for the model. To create a real link in the knowledge graph, the agent must write Markdown that contains an explicit `[[Note Title]]` wiki link and then rebuild the index.
193
+
194
+ When adding memory, follow this contract:
195
+
196
+ - Link the new note to at least one existing note when there is a related concept.
197
+ - Use the exact target note title inside `[[...]]`.
198
+ - Add retrieval tags such as `#architecture`, `#decision`, `#runbook` or `#preference`.
199
+ - Do not leave isolated notes unless they are intentionally root concepts.
200
+
180
201
  Rebuild the index:
181
202
 
182
203
  ```bash
@@ -199,9 +220,9 @@ Use this loop during real work:
199
220
  2. Run `blink context "<task>" --agent "$BLINK_AGENT" --json`.
200
221
  3. Use returned sources as project memory.
201
222
  4. Perform the task.
202
- 5. Save only durable learnings with `blink add`.
223
+ 5. Save only durable learnings with `blink add`, including `[[wiki links]]` to related notes.
203
224
  6. Run `blink index`.
204
- 7. Validate with `blink validate`.
225
+ 7. Validate with `blink validate`, `blink broken-links` and `blink orphans` when graph links matter.
205
226
 
206
227
  Do not store secrets, credentials, private keys, access tokens or transient chat noise.
207
228
 
@@ -313,50 +334,36 @@ This allows `coding-agent` and `research-agent` to both have a note named `Archi
313
334
 
314
335
  ## MCP Server Integration
315
336
 
316
- Brainlink is not an MCP server. It is a CLI-first memory engine.
317
-
318
- An MCP server can use Brainlink by spawning `blink` or `brainlink` as a subprocess and reading `--json` output. This keeps Brainlink decoupled from any specific MCP SDK while still making it usable by MCP-compatible agents.
319
-
320
- Minimum integration contract:
337
+ Brainlink ships a stdio MCP server with the npm package:
321
338
 
322
339
  ```bash
323
- blink context "<task>" --agent "$BLINK_AGENT" --json
324
- blink add "Decision Title" --agent "$BLINK_AGENT" --content "Durable memory. #decision"
325
- blink index
340
+ brainlink-mcp
326
341
  ```
327
342
 
328
- Example Node.js wrapper inside an external MCP server:
329
-
330
- ```js
331
- import { execFile } from 'node:child_process'
332
- import { promisify } from 'node:util'
333
-
334
- const execFileAsync = promisify(execFile)
343
+ Example MCP client configuration:
335
344
 
336
- export const brainlinkContext = async ({ vault, agent, query }) => {
337
- const { stdout } = await execFileAsync('blink', [
338
- 'context',
339
- query,
340
- '--vault',
341
- vault,
342
- '--agent',
343
- agent,
344
- '--mode',
345
- 'hybrid',
346
- '--json'
347
- ])
348
-
349
- return JSON.parse(stdout)
345
+ ```json
346
+ {
347
+ "mcpServers": {
348
+ "brainlink": {
349
+ "command": "brainlink-mcp"
350
+ }
351
+ }
350
352
  }
351
353
  ```
352
354
 
353
- Recommended MCP tools exposed by the external server:
355
+ Available tools:
356
+
357
+ - `brainlink_context`: read indexed context for a task or question.
358
+ - `brainlink_search`: search indexed notes.
359
+ - `brainlink_add_note`: write durable Markdown memory and reindex.
360
+ - `brainlink_index`: rebuild the vault index.
361
+ - `brainlink_validate`: validate broken links and orphan notes.
362
+ - `brainlink_graph`: read indexed graph nodes and links.
363
+ - `brainlink_broken_links`: list unresolved wiki links.
364
+ - `brainlink_orphans`: list disconnected notes.
354
365
 
355
- - `brainlink_context`: calls `blink context ... --json`.
356
- - `brainlink_search`: calls `blink search ... --json`.
357
- - `brainlink_add_note`: calls `blink add ... --json`, then `blink index`.
358
- - `brainlink_graph`: calls `blink graph ... --json`.
359
- - `brainlink_validate`: calls `blink validate ... --json`.
366
+ The same linking rule applies through MCP: `brainlink_context` is read-only, and real graph links require Markdown notes with explicit `[[wiki links]]` followed by indexing.
360
367
 
361
368
  ## Graph UI
362
369
 
@@ -388,7 +395,7 @@ blink server --vault ./vault --no-index
388
395
 
389
396
  The HTTP API is read-only and exists only to power the graph UI and local inspection workflows.
390
397
 
391
- The server refuses non-loopback hosts by default. Use `--allow-public` only behind your own authentication, authorization and TLS.
398
+ The server always refuses non-loopback hosts. Brainlink HTTP only runs on localhost.
392
399
 
393
400
  Routes:
394
401
 
@@ -563,7 +570,7 @@ blink server --vault ./vault --watch
563
570
 
564
571
  Starts the local read-only graph UI and HTTP API.
565
572
 
566
- To bind outside localhost, pass `--allow-public` and put the server behind your own auth and TLS.
573
+ The HTTP server only binds to loopback hosts such as `127.0.0.1`, `localhost` or `::1`.
567
574
 
568
575
  ## Machine-Readable Output
569
576
 
@@ -691,7 +698,6 @@ Detailed notes:
691
698
  - Semantic search uses SQLite embedding buckets to narrow candidates before cosine scoring.
692
699
  - `embeddingProvider` currently supports `local` and `none`.
693
700
  - Link resolution is title-based inside each agent namespace, with `shared` as fallback.
694
- - No embedded MCP server is shipped; MCP integration is done by external servers wrapping the CLI.
695
701
  - HTTP API is local and unauthenticated.
696
702
  - Watch mode depends on the platform filesystem watcher.
697
703
 
@@ -712,6 +718,7 @@ The alpha includes local semantic retrieval. Remote embedding providers, remote
712
718
  Brainlink is local-first by default.
713
719
 
714
720
  - Do not expose the HTTP server publicly without authentication.
721
+ - Brainlink HTTP is localhost-only and refuses non-loopback hosts.
715
722
  - Brainlink blocks common secret patterns by default when adding notes. Use `--allow-sensitive` only for intentional, protected vaults.
716
723
  - Do not store secrets, credentials, API keys or regulated personal data unless the vault is protected by your own storage controls.
717
724
  - Treat `.brainlink/brainlink.db` as disposable derived data.
package/SECURITY.md CHANGED
@@ -5,7 +5,7 @@ Brainlink is local-first.
5
5
  ## Defaults
6
6
 
7
7
  - The HTTP server binds to `127.0.0.1` by default.
8
- - The HTTP server refuses non-loopback hosts unless `--allow-public` is passed.
8
+ - The HTTP server always refuses non-loopback hosts.
9
9
  - The HTTP server is read-only and does not expose note creation, indexing or update routes.
10
10
  - The SQLite database is a derived local index.
11
11
  - Markdown files are user-owned source data.
@@ -14,7 +14,7 @@ Brainlink is local-first.
14
14
 
15
15
  ## Remote Exposure
16
16
 
17
- Do not expose the HTTP server on a public interface without adding authentication, authorization and transport security.
17
+ Brainlink HTTP is intentionally localhost-only. It does not support binding to a public interface.
18
18
 
19
19
  ## Sensitive Memory
20
20
 
@@ -0,0 +1,25 @@
1
+ <svg width="900" height="220" viewBox="0 0 900 220" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
2
+ <title id="title">Brainlink</title>
3
+ <desc id="desc">Brainlink logo with a linked memory graph mark and wordmark.</desc>
4
+ <rect width="900" height="220" fill="transparent"/>
5
+ <g transform="translate(42 24)">
6
+ <rect x="6" y="6" width="160" height="160" rx="36" fill="#F8FAFC"/>
7
+ <rect x="6" y="6" width="160" height="160" rx="36" stroke="#111827" stroke-width="8"/>
8
+ <path d="M47 84C47 65.225 62.225 50 81 50H92C110.775 50 126 65.225 126 84C126 102.775 110.775 118 92 118H81C62.225 118 47 102.775 47 84Z" stroke="#111827" stroke-width="12" stroke-linecap="round"/>
9
+ <path d="M67 84C67 76.268 73.268 70 81 70H92C99.732 70 106 76.268 106 84C106 91.732 99.732 98 92 98H81C73.268 98 67 91.732 67 84Z" fill="#FFFFFF"/>
10
+ <path d="M54 132L43 121L54 110" stroke="#2563EB" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
11
+ <path d="M118 110L129 121L118 132" stroke="#2563EB" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
12
+ <circle cx="55" cy="43" r="12" fill="#14B8A6"/>
13
+ <circle cx="121" cy="43" r="12" fill="#2563EB"/>
14
+ <circle cx="86" cy="136" r="12" fill="#111827"/>
15
+ <path d="M66.5 47.5L109.5 47.5" stroke="#94A3B8" stroke-width="7" stroke-linecap="round"/>
16
+ <path d="M62 54L79 125" stroke="#94A3B8" stroke-width="7" stroke-linecap="round"/>
17
+ <path d="M115 54L92 125" stroke="#94A3B8" stroke-width="7" stroke-linecap="round"/>
18
+ </g>
19
+ <g transform="translate(244 68)">
20
+ <text x="0" y="62" fill="#111827" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="72" font-weight="760">Brainlink</text>
21
+ <text x="4" y="102" fill="#475569" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="24" font-weight="500">Local-first memory for AI agents</text>
22
+ <path d="M0 124H268" stroke="#14B8A6" stroke-width="8" stroke-linecap="round"/>
23
+ <path d="M292 124H392" stroke="#2563EB" stroke-width="8" stroke-linecap="round"/>
24
+ </g>
25
+ </svg>
@@ -1,6 +1,6 @@
1
1
  export const isLoopbackHost = (host) => host === 'localhost' || host === '::1' || host === '[::1]' || host.startsWith('127.');
2
- export const assertPublicBindAllowed = (host, allowPublic = false) => {
3
- if (!allowPublic && !isLoopbackHost(host)) {
4
- throw new Error(`Refusing to bind Brainlink server to non-loopback host ${host}. Pass --allow-public only behind your own auth and TLS.`);
2
+ export const assertLoopbackHost = (host) => {
3
+ if (!isLoopbackHost(host)) {
4
+ throw new Error(`Refusing to bind Brainlink server to non-loopback host ${host}. Brainlink HTTP only runs on localhost.`);
5
5
  }
6
6
  };
@@ -1,11 +1,11 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { indexVault } from './index-vault.js';
3
3
  import { startVaultWatcher } from './watch-vault.js';
4
- import { assertPublicBindAllowed } from './server/host-security.js';
4
+ import { assertLoopbackHost } from './server/host-security.js';
5
5
  import { contentTypes, createJsonResponse, isHttpError } from './server/http.js';
6
6
  import { route } from './server/routes.js';
7
7
  export const startServer = async (input) => {
8
- assertPublicBindAllowed(input.host, input.allowPublic);
8
+ assertLoopbackHost(input.host);
9
9
  if (input.shouldIndex) {
10
10
  await indexVault(input.vaultPath);
11
11
  }
@@ -89,7 +89,6 @@ export const registerWriteCommands = (program) => {
89
89
  .option('-p, --port <port>', 'server port', '4321')
90
90
  .option('--no-index', 'skip indexing before starting the server')
91
91
  .option('-w, --watch', 'watch markdown files and reindex on changes')
92
- .option('--allow-public', 'allow binding the server to a non-loopback host')
93
92
  .option('--json', 'print machine-readable JSON')
94
93
  .description('start a local web UI for the knowledge graph')
95
94
  .action(async (options) => {
@@ -99,8 +98,7 @@ export const registerWriteCommands = (program) => {
99
98
  host: options.host ?? resolved.config.host,
100
99
  port: parsePositiveInteger(options.port ?? String(resolved.config.port), resolved.config.port),
101
100
  shouldIndex: options.index,
102
- shouldWatch: Boolean(options.watch),
103
- allowPublic: Boolean(options.allowPublic)
101
+ shouldWatch: Boolean(options.watch)
104
102
  });
105
103
  print(options.json, { url: server.url, watch: Boolean(options.watch), readonly: true }, () => `Brainlink graph server running at ${server.url}`);
106
104
  });
package/dist/cli/main.js CHANGED
@@ -1,8 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { basename } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
4
6
  import { registerReadCommands } from './commands/read-commands.js';
5
7
  import { registerWriteCommands } from './commands/write-commands.js';
8
+ const readPackageVersion = () => {
9
+ const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
10
+ const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
11
+ return metadata.version ?? '0.0.0';
12
+ };
6
13
  const program = new Command();
7
14
  const cliName = basename(process.argv[1] ?? 'brainlink');
8
15
  const displayName = cliName === 'blink' ? 'blink' : 'brainlink';
@@ -11,7 +18,7 @@ program
11
18
  .name(displayName)
12
19
  .alias(aliasName)
13
20
  .description('Local-first knowledge memory for agents')
14
- .version('0.1.0');
21
+ .version(readPackageVersion());
15
22
  registerWriteCommands(program);
16
23
  registerReadCommands(program);
17
24
  program.parseAsync().catch((error) => {
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createBrainlinkMcpServer } from './server.js';
4
+ const server = createBrainlinkMcpServer();
5
+ const transport = new StdioServerTransport();
6
+ server.connect(transport).catch((error) => {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+ console.error(message);
9
+ process.exitCode = 1;
10
+ });
@@ -0,0 +1,59 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { addNoteInputSchema, addNoteTool, brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, searchInputSchema, searchTool, validateInputSchema, validateTool } from './tools.js';
6
+ const readPackageVersion = () => {
7
+ const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
8
+ const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
9
+ return metadata.version ?? '0.0.0';
10
+ };
11
+ export const createBrainlinkMcpServer = () => {
12
+ const server = new McpServer({
13
+ name: 'brainlink',
14
+ title: 'Brainlink',
15
+ version: readPackageVersion(),
16
+ description: 'Local-first Markdown memory tools for AI agents.'
17
+ });
18
+ server.registerTool('brainlink_context', {
19
+ title: 'Build Brainlink Context',
20
+ description: 'Read indexed Brainlink memory for a task or question. This is read-only and does not create graph links.',
21
+ inputSchema: contextInputSchema
22
+ }, contextTool);
23
+ server.registerTool('brainlink_search', {
24
+ title: 'Search Brainlink Memory',
25
+ description: 'Search indexed Brainlink notes with FTS, semantic or hybrid retrieval.',
26
+ inputSchema: searchInputSchema
27
+ }, searchTool);
28
+ server.registerTool('brainlink_add_note', {
29
+ title: 'Add Brainlink Note',
30
+ description: 'Write durable Markdown memory, then reindex the vault. Include explicit [[wiki links]] for connected graph memory.',
31
+ inputSchema: addNoteInputSchema
32
+ }, addNoteTool);
33
+ server.registerTool('brainlink_index', {
34
+ title: 'Index Brainlink Vault',
35
+ description: 'Rebuild the local Brainlink index from Markdown notes.',
36
+ inputSchema: indexInputSchema
37
+ }, indexTool);
38
+ server.registerTool('brainlink_validate', {
39
+ title: 'Validate Brainlink Vault',
40
+ description: 'Validate indexed graph health, including broken links and orphan notes.',
41
+ inputSchema: validateInputSchema
42
+ }, validateTool);
43
+ server.registerTool('brainlink_graph', {
44
+ title: 'Read Brainlink Graph',
45
+ description: 'Read indexed graph nodes and wiki-link edges.',
46
+ inputSchema: graphInputSchema
47
+ }, graphTool);
48
+ server.registerTool('brainlink_broken_links', {
49
+ title: 'List Brainlink Broken Links',
50
+ description: 'List unresolved indexed wiki links.',
51
+ inputSchema: brokenLinksInputSchema
52
+ }, brokenLinksTool);
53
+ server.registerTool('brainlink_orphans', {
54
+ title: 'List Brainlink Orphans',
55
+ description: 'List indexed notes without incoming or outgoing graph links.',
56
+ inputSchema: orphansInputSchema
57
+ }, orphansTool);
58
+ return server;
59
+ };
@@ -0,0 +1,166 @@
1
+ import { z } from 'zod';
2
+ import { getBrokenLinksReport, getOrphansReport, validateVault } from '../application/analyze-vault.js';
3
+ import { addNote } from '../application/add-note.js';
4
+ import { buildContextPackage } from '../application/build-context.js';
5
+ import { getGraph } from '../application/get-graph.js';
6
+ import { indexVault } from '../application/index-vault.js';
7
+ import { searchKnowledge } from '../application/search-knowledge.js';
8
+ import { sanitizeSearchMode } from '../infrastructure/config.js';
9
+ import { loadBrainlinkConfig } from '../infrastructure/config.js';
10
+ import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
11
+ const positiveInteger = (fallback) => z
12
+ .number()
13
+ .int()
14
+ .positive()
15
+ .optional()
16
+ .transform((value) => value ?? fallback);
17
+ const vaultInput = {
18
+ vault: z.string().min(1).optional().describe('Vault directory. Omit to use the configured Brainlink default vault.')
19
+ };
20
+ const agentInput = {
21
+ agent: z.string().min(1).optional().describe('Agent memory namespace. Omit to read shared/default indexed memory.')
22
+ };
23
+ const searchModeInput = {
24
+ mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
25
+ };
26
+ const resolveVault = async (vault) => {
27
+ const config = await loadBrainlinkConfig();
28
+ return assertVaultAllowed(vault ?? config.vault, config.allowedVaults);
29
+ };
30
+ const jsonResult = (value) => ({
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: JSON.stringify(value, null, 2)
35
+ }
36
+ ],
37
+ structuredContent: value
38
+ });
39
+ export const contextInputSchema = {
40
+ ...vaultInput,
41
+ ...agentInput,
42
+ ...searchModeInput,
43
+ query: z.string().min(1).describe('Task or question to retrieve Brainlink context for.'),
44
+ limit: positiveInteger(12).describe('Maximum search results before context selection.'),
45
+ tokens: positiveInteger(2000).describe('Maximum estimated context tokens.')
46
+ };
47
+ export const searchInputSchema = {
48
+ ...vaultInput,
49
+ ...agentInput,
50
+ ...searchModeInput,
51
+ query: z.string().min(1).describe('Search query.'),
52
+ limit: positiveInteger(10).describe('Maximum result count.')
53
+ };
54
+ export const addNoteInputSchema = {
55
+ ...vaultInput,
56
+ title: z.string().min(1).describe('Markdown note title.'),
57
+ content: z
58
+ .string()
59
+ .min(1)
60
+ .describe('Durable Markdown memory. Include explicit [[wiki links]] and #tags when the memory should be connected.'),
61
+ agent: z.string().min(1).optional().default('shared').describe('Agent memory namespace. Defaults to shared.'),
62
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
63
+ };
64
+ export const indexInputSchema = {
65
+ ...vaultInput
66
+ };
67
+ export const validateInputSchema = {
68
+ ...vaultInput,
69
+ ...agentInput
70
+ };
71
+ export const graphInputSchema = {
72
+ ...vaultInput,
73
+ ...agentInput
74
+ };
75
+ export const brokenLinksInputSchema = {
76
+ ...vaultInput,
77
+ ...agentInput
78
+ };
79
+ export const orphansInputSchema = {
80
+ ...vaultInput,
81
+ ...agentInput
82
+ };
83
+ export const contextTool = async (input) => {
84
+ const vault = await resolveVault(input.vault);
85
+ const config = await loadBrainlinkConfig();
86
+ const mode = sanitizeSearchMode(input.mode, config.defaultSearchMode);
87
+ const contextPackage = await buildContextPackage(vault, input.query, input.limit, input.tokens, input.agent, mode);
88
+ return jsonResult({
89
+ vault,
90
+ agent: input.agent,
91
+ mode,
92
+ ...contextPackage
93
+ });
94
+ };
95
+ export const searchTool = async (input) => {
96
+ const vault = await resolveVault(input.vault);
97
+ const config = await loadBrainlinkConfig();
98
+ const mode = sanitizeSearchMode(input.mode, config.defaultSearchMode);
99
+ const results = await searchKnowledge(vault, input.query, input.limit, input.agent, mode);
100
+ return jsonResult({
101
+ vault,
102
+ agent: input.agent,
103
+ query: input.query,
104
+ limit: input.limit,
105
+ mode,
106
+ results
107
+ });
108
+ };
109
+ export const addNoteTool = async (input) => {
110
+ const vault = await resolveVault(input.vault);
111
+ const path = await addNote(vault, input.title, input.content, input.agent, {
112
+ allowSensitive: input.allowSensitive
113
+ });
114
+ const index = await indexVault(vault);
115
+ return jsonResult({
116
+ vault,
117
+ title: input.title,
118
+ agent: input.agent,
119
+ path,
120
+ index
121
+ });
122
+ };
123
+ export const indexTool = async (input) => {
124
+ const vault = await resolveVault(input.vault);
125
+ const result = await indexVault(vault);
126
+ return jsonResult({
127
+ vault,
128
+ ...result
129
+ });
130
+ };
131
+ export const validateTool = async (input) => {
132
+ const vault = await resolveVault(input.vault);
133
+ const validation = await validateVault(vault, input.agent);
134
+ return jsonResult({
135
+ vault,
136
+ agent: input.agent,
137
+ ...validation
138
+ });
139
+ };
140
+ export const graphTool = async (input) => {
141
+ const vault = await resolveVault(input.vault);
142
+ const graph = await getGraph(vault, input.agent);
143
+ return jsonResult({
144
+ vault,
145
+ agent: input.agent,
146
+ ...graph
147
+ });
148
+ };
149
+ export const brokenLinksTool = async (input) => {
150
+ const vault = await resolveVault(input.vault);
151
+ const brokenLinks = await getBrokenLinksReport(vault, input.agent);
152
+ return jsonResult({
153
+ vault,
154
+ agent: input.agent,
155
+ brokenLinks
156
+ });
157
+ };
158
+ export const orphansTool = async (input) => {
159
+ const vault = await resolveVault(input.vault);
160
+ const orphans = await getOrphansReport(vault, input.agent);
161
+ return jsonResult({
162
+ vault,
163
+ agent: input.agent,
164
+ orphans
165
+ });
166
+ };
@@ -138,6 +138,41 @@ Rules:
138
138
  - Prefer summaries over raw transcripts.
139
139
  - Preserve dates when the timing matters.
140
140
 
141
+ ## Linking Contract
142
+
143
+ Brainlink only builds graph edges from Markdown `[[wiki links]]`.
144
+
145
+ The `context` command is read-only. It retrieves indexed notes and returns a compact package for the model, but it does not write memory, create backlinks, infer relationships or modify the graph. If an agent reads context and then learns something durable, the agent must write a note with explicit links before that knowledge becomes connected memory.
146
+
147
+ Required write behavior:
148
+
149
+ 1. Choose a clear title for the new note.
150
+ 2. Look for an existing related concept with `search`, `links` or `backlinks`.
151
+ 3. Add at least one `[[Existing Note Title]]` link unless the note is intentionally a root concept.
152
+ 4. Add useful `#tags` for retrieval.
153
+ 5. Run `index` after the write.
154
+ 6. Run `validate`, `broken-links` or `orphans` when the graph should be connected.
155
+
156
+ Good linked note:
157
+
158
+ ```bash
159
+ blink add "SQLite Index Rebuild" \
160
+ --agent coding-agent \
161
+ --content "Legacy derived indexes without agent columns are rebuilt because SQLite is disposable. Related: [[Architecture]], [[Agent Namespaces]]. #sqlite #architecture #decision"
162
+ blink index
163
+ blink validate --agent coding-agent
164
+ ```
165
+
166
+ Poor disconnected note:
167
+
168
+ ```bash
169
+ blink add "SQLite Index Rebuild" \
170
+ --agent coding-agent \
171
+ --content "We rebuild old indexes now."
172
+ ```
173
+
174
+ The poor note may be searchable, but it will not create graph links, backlinks or useful traversal paths.
175
+
141
176
  ## Read Policy
142
177
 
143
178
  Before answering a memory-dependent question, run:
@@ -396,19 +431,38 @@ blink watch --vault ./vault
396
431
 
397
432
  This process watches Markdown files and rebuilds the index after changes.
398
433
 
399
- ### Use From An External MCP Server
434
+ ### Use From MCP
435
+
436
+ Brainlink ships a stdio MCP server:
437
+
438
+ ```bash
439
+ brainlink-mcp
440
+ ```
441
+
442
+ Example MCP client configuration:
400
443
 
401
- Brainlink does not ship an MCP server. An MCP server can use Brainlink by executing the CLI and parsing `--json`.
444
+ ```json
445
+ {
446
+ "mcpServers": {
447
+ "brainlink": {
448
+ "command": "brainlink-mcp"
449
+ }
450
+ }
451
+ }
452
+ ```
402
453
 
403
- Recommended wrapper mapping:
454
+ Available MCP tools:
404
455
 
405
- - `brainlink_context`: run `blink context "<query>" --vault <vault> --agent <agent> --mode hybrid --json`.
406
- - `brainlink_search`: run `blink search "<query>" --vault <vault> --agent <agent> --mode hybrid --json`.
407
- - `brainlink_add_note`: run `blink add "<title>" --vault <vault> --agent <agent> --content "<content>" --json`, then `blink index`.
408
- - `brainlink_graph`: run `blink graph --vault <vault> --agent <agent> --json`.
409
- - `brainlink_validate`: run `blink validate --vault <vault> --agent <agent> --json`.
456
+ - `brainlink_context`
457
+ - `brainlink_search`
458
+ - `brainlink_add_note`
459
+ - `brainlink_index`
460
+ - `brainlink_validate`
461
+ - `brainlink_graph`
462
+ - `brainlink_broken_links`
463
+ - `brainlink_orphans`
410
464
 
411
- External wrappers should set `BRAINLINK_ALLOWED_VAULTS` before invoking the CLI:
465
+ MCP clients can pass `vault` and `agent` arguments per tool call. Set `BRAINLINK_ALLOWED_VAULTS` when exposing Brainlink to an external agent process so a tool cannot pass arbitrary vault paths:
412
466
 
413
467
  ```bash
414
468
  export BRAINLINK_ALLOWED_VAULTS="/absolute/path/to/project-vault"
@@ -453,6 +507,12 @@ Output:
453
507
 
454
508
  Agents should include source paths in their reasoning or final answer when the user needs traceability.
455
509
 
510
+ Non-goals:
511
+
512
+ - `context` must not be treated as a write operation.
513
+ - Retrieved context must not be assumed to create graph edges.
514
+ - Backlinks are derived only from indexed `[[wiki links]]`.
515
+
456
516
  ## Operational Rules
457
517
 
458
518
  - Re-run `index` after modifying notes.
@@ -461,6 +521,8 @@ Agents should include source paths in their reasoning or final answer when the u
461
521
  - Do not manually edit the database.
462
522
  - Keep generated context short enough for the target model.
463
523
  - Prefer specific queries over broad queries.
524
+ - Write explicit `[[wiki links]]` when durable memory should be connected.
525
+ - Check `orphans` before assuming the graph is healthy.
464
526
 
465
527
  ## Failure Modes
466
528
 
@@ -488,6 +550,6 @@ Weak retrieval usually means:
488
550
 
489
551
  - Search supports FTS, local semantic embeddings, SQLite semantic buckets and hybrid ranking.
490
552
  - Local embeddings are deterministic and provider-free; remote embedding providers are not implemented yet.
491
- - MCP integration is external: wrap the CLI from your own MCP server.
553
+ - MCP integration is available through the `brainlink-mcp` stdio server.
492
554
  - HTTP API is local and unauthenticated.
493
555
  - Watch mode depends on platform filesystem watcher behavior.
@@ -167,20 +167,18 @@ HTTP request
167
167
 
168
168
  The HTTP API is local-first and unauthenticated. It is meant for local agents, browser UI, and development workflows.
169
169
 
170
- ## External MCP Flow
170
+ ## MCP Flow
171
171
 
172
- Brainlink does not contain an MCP server. MCP compatibility is achieved by an external MCP server wrapping the CLI.
172
+ Brainlink includes a stdio MCP server for agent integrations.
173
173
 
174
174
  ```txt
175
175
  MCP client
176
- -> external MCP server
177
- -> child_process execFile("blink", ["context", ..., "--json"])
178
- -> Brainlink CLI
176
+ -> brainlink-mcp
179
177
  -> application use case
180
- -> JSON stdout
178
+ -> MCP tool result
181
179
  ```
182
180
 
183
- This keeps the package CLI-first and avoids coupling the core project to one MCP SDK.
181
+ The MCP adapter stays thin. It validates tool inputs, resolves the configured vault and calls the same application use cases used by the CLI.
184
182
 
185
183
  ## Link Resolution
186
184
 
package/docs/RELEASE.md CHANGED
@@ -32,7 +32,7 @@ blink context "release smoke" --vault ./tmp-vault --mode hybrid --json
32
32
  blink server --vault ./tmp-vault --host 127.0.0.1 --port 4321
33
33
  ```
34
34
 
35
- 9. Verify the server refuses accidental public binds:
35
+ 9. Verify the server refuses public binds:
36
36
 
37
37
  ```bash
38
38
  blink server --vault ./tmp-vault --host 0.0.0.0
@@ -47,10 +47,18 @@ blink server --vault ./tmp-vault --host 0.0.0.0
47
47
 
48
48
  The preferred path is the `Publish npm` GitHub Actions workflow:
49
49
 
50
+ - Push to `main`: runs checks, pack smoke, then publishes the package to npm with `latest` when `package.json` contains a version that is not already published.
50
51
  - GitHub Release `published`: runs checks, pack smoke, then publishes to npm with provenance.
51
52
  - Manual `workflow_dispatch`: runs a dry run by default. Disable `dry_run` only for an intentional manual publish.
53
+ - Manual `workflow_dispatch` accepts an optional `dist_tag` override. Use `latest` only when the default npm install command should resolve to that version.
52
54
  - Prerelease versions publish under their prerelease dist-tag, for example `0.1.0-alpha.1` publishes with `--tag alpha`.
53
55
 
56
+ On `main`, the publish job checks npm before publishing. If the version already exists, it automatically bumps the package inside the runner to the next available version before checks, packing and publishing. For example, `0.1.0-alpha.4` becomes `0.1.0-alpha.5`.
57
+
58
+ The automatic bump is intentionally not pushed back to `main`. The branch stays protected, and npm remains the source of truth for the latest published package version.
59
+
60
+ Manual and GitHub Release publishes do not auto-bump. If their version already exists, they skip `npm publish` because npm versions are immutable.
61
+
54
62
  For emergency local publishing of scoped public packages:
55
63
 
56
64
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.10",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,10 +25,12 @@
25
25
  ],
26
26
  "bin": {
27
27
  "brainlink": "dist/cli/main.js",
28
- "blink": "dist/cli/main.js"
28
+ "blink": "dist/cli/main.js",
29
+ "brainlink-mcp": "dist/mcp/main.js"
29
30
  },
30
31
  "files": [
31
32
  "dist",
33
+ "assets",
32
34
  "README.md",
33
35
  "LICENSE",
34
36
  "CHANGELOG.md",
@@ -44,6 +46,7 @@
44
46
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
45
47
  "build": "npm run clean && tsc -p tsconfig.json",
46
48
  "dev": "tsx src/cli/main.ts",
49
+ "dev:mcp": "tsx src/mcp/main.ts",
47
50
  "test": "vitest run --config vitest.config.ts",
48
51
  "check": "npm run build && npm run test",
49
52
  "benchmark:large": "tsx src/benchmarks/large-vault.ts",
@@ -51,8 +54,10 @@
51
54
  "pack:smoke": "npm pack --dry-run"
52
55
  },
53
56
  "dependencies": {
57
+ "@modelcontextprotocol/sdk": "^1.29.0",
54
58
  "better-sqlite3": "^12.9.0",
55
- "commander": "^14.0.2"
59
+ "commander": "^14.0.2",
60
+ "zod": "^4.3.6"
56
61
  },
57
62
  "devDependencies": {
58
63
  "@types/better-sqlite3": "^7.6.13",