@andespindola/brainlink 0.1.0-beta.166 → 0.1.0-beta.167

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
@@ -1049,6 +1049,89 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
1049
1049
  `autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
1050
1050
  `autoCanonicalContextLinks` is optional and defaults to `true`. When enabled, `blink add`, `brainlink_add_note` and `brainlink_add_file` add a canonical `## Context Links` entry to the inferred context hub, creating that hub when needed.
1051
1051
 
1052
+ ## Remote MCP Server
1053
+
1054
+ Brainlink can run as a centralized MCP service for clustered workloads. This keeps one shared memory service inside the cluster while applications and agents connect to the MCP endpoint over Streamable HTTP.
1055
+
1056
+ ```bash
1057
+ BRAINLINK_MCP_TOKEN="change-me" brainlink mcp-server \
1058
+ --vault /data/vault \
1059
+ --host 0.0.0.0 \
1060
+ --port 3333 \
1061
+ --path /mcp
1062
+ ```
1063
+
1064
+ The server exposes:
1065
+
1066
+ ```txt
1067
+ POST /mcp MCP Streamable HTTP endpoint
1068
+ GET /healthz liveness probe
1069
+ GET /readyz readiness probe
1070
+ ```
1071
+
1072
+ When `BRAINLINK_MCP_TOKEN` or `--token` is set, MCP requests must include:
1073
+
1074
+ ```txt
1075
+ Authorization: Bearer <token>
1076
+ ```
1077
+
1078
+ For Kubernetes, run Brainlink as a central `Deployment` with a `Service` and a mounted vault volume:
1079
+
1080
+ ```yaml
1081
+ apiVersion: apps/v1
1082
+ kind: Deployment
1083
+ metadata:
1084
+ name: brainlink-mcp
1085
+ spec:
1086
+ replicas: 1
1087
+ selector:
1088
+ matchLabels:
1089
+ app: brainlink-mcp
1090
+ template:
1091
+ metadata:
1092
+ labels:
1093
+ app: brainlink-mcp
1094
+ spec:
1095
+ containers:
1096
+ - name: brainlink
1097
+ image: brainlink:latest
1098
+ args: ["brainlink", "mcp-server", "--vault", "/data/vault", "--host", "0.0.0.0", "--port", "3333"]
1099
+ env:
1100
+ - name: BRAINLINK_MCP_TOKEN
1101
+ valueFrom:
1102
+ secretKeyRef:
1103
+ name: brainlink-mcp
1104
+ key: token
1105
+ ports:
1106
+ - containerPort: 3333
1107
+ readinessProbe:
1108
+ httpGet:
1109
+ path: /readyz
1110
+ port: 3333
1111
+ livenessProbe:
1112
+ httpGet:
1113
+ path: /healthz
1114
+ port: 3333
1115
+ volumeMounts:
1116
+ - name: vault
1117
+ mountPath: /data/vault
1118
+ volumes:
1119
+ - name: vault
1120
+ persistentVolumeClaim:
1121
+ claimName: brainlink-vault
1122
+ ---
1123
+ apiVersion: v1
1124
+ kind: Service
1125
+ metadata:
1126
+ name: brainlink-mcp
1127
+ spec:
1128
+ selector:
1129
+ app: brainlink-mcp
1130
+ ports:
1131
+ - port: 3333
1132
+ targetPort: 3333
1133
+ ```
1134
+
1052
1135
  Use `"embeddingProvider": "none"` when you want FTS-only indexing.
1053
1136
 
1054
1137
  For local security checks, set your Snyk token in the environment:
@@ -20,6 +20,7 @@ import { loadBrainlinkConfig } from '../../infrastructure/config.js';
20
20
  import { assertVaultAllowed, ensureVault, isBucketVaultPath } from '../../infrastructure/file-system-vault.js';
21
21
  import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
22
22
  import { addVolatileMemory, clearVolatileMemory } from '../../infrastructure/volatile-memory.js';
23
+ import { startRemoteMcpServer } from '../../mcp/http-server.js';
23
24
  import { installAgentIntegration } from './agent-commands.js';
24
25
  import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
25
26
  const resolveAddContent = (options) => {
@@ -1105,6 +1106,38 @@ export const registerWriteCommands = (program) => {
1105
1106
  ? ' (auto-open disabled)'
1106
1107
  : ''}`);
1107
1108
  });
1109
+ program
1110
+ .command('mcp-server')
1111
+ .option('-v, --vault <vault>', 'vault directory')
1112
+ .option('-a, --agent <agent>', 'agent memory namespace')
1113
+ .option('-h, --host <host>', 'remote MCP server host', '0.0.0.0')
1114
+ .option('-p, --port <port>', 'remote MCP server port', '3333')
1115
+ .option('--path <path>', 'remote MCP endpoint path', '/mcp')
1116
+ .option('--token <token>', 'bearer token required for MCP requests')
1117
+ .option('--no-index', 'skip indexing before starting the MCP server')
1118
+ .option('--json', 'print machine-readable JSON')
1119
+ .description('start a remote MCP server for centralized cluster access')
1120
+ .action(async (options) => {
1121
+ const resolved = await resolveOptions(options);
1122
+ const token = options.token ?? process.env.BRAINLINK_MCP_TOKEN;
1123
+ const server = await startRemoteMcpServer({
1124
+ vaultPath: resolved.vault,
1125
+ agent: resolved.agent,
1126
+ host: options.host ?? '0.0.0.0',
1127
+ port: parsePositiveInteger(options.port ?? '3333', 3333),
1128
+ path: options.path ?? '/mcp',
1129
+ token,
1130
+ shouldIndex: options.index
1131
+ });
1132
+ print(options.json, {
1133
+ url: server.url,
1134
+ healthUrl: server.healthUrl,
1135
+ readyUrl: server.readyUrl,
1136
+ vault: resolved.vault,
1137
+ agent: resolved.agent ?? '*',
1138
+ auth: token === undefined ? 'disabled' : 'bearer'
1139
+ }, () => `Brainlink remote MCP server running at ${server.url} (health: ${server.healthUrl}, readiness: ${server.readyUrl})`);
1140
+ });
1108
1141
  program
1109
1142
  .command('quickstart')
1110
1143
  .option('-v, --vault <vault>', 'vault directory')
@@ -0,0 +1,97 @@
1
+ import { createServer } from 'node:http';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { indexVault } from '../application/index-vault.js';
4
+ import { createBrainlinkMcpServer } from './server.js';
5
+ import { runStartupBootstrap } from './startup.js';
6
+ const normalizePath = (path) => {
7
+ const trimmed = path.trim();
8
+ if (trimmed.length === 0) {
9
+ return '/mcp';
10
+ }
11
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
12
+ };
13
+ const writeJson = (response, statusCode, value) => {
14
+ response.writeHead(statusCode, {
15
+ 'content-type': 'application/json; charset=utf-8',
16
+ 'cache-control': 'no-store'
17
+ });
18
+ response.end(`${JSON.stringify(value)}\n`);
19
+ };
20
+ const readBearerToken = (authorization) => {
21
+ const value = Array.isArray(authorization) ? authorization[0] : authorization;
22
+ const match = value?.match(/^Bearer\s+(.+)$/i);
23
+ return match?.[1]?.trim();
24
+ };
25
+ const isAuthorized = (expectedToken, authorization) => expectedToken === undefined || readBearerToken(authorization) === expectedToken;
26
+ export const startRemoteMcpServer = async (input) => {
27
+ const mcpPath = normalizePath(input.path);
28
+ const startup = await runStartupBootstrap();
29
+ if (input.shouldIndex) {
30
+ await indexVault(input.vaultPath);
31
+ }
32
+ if (startup.error) {
33
+ console.error(`Brainlink MCP startup bootstrap warning: ${startup.error}`);
34
+ }
35
+ const server = createServer(async (request, response) => {
36
+ const url = new URL(request.url ?? '/', `http://${request.headers.host ?? input.host}`);
37
+ if (url.pathname === '/healthz') {
38
+ writeJson(response, 200, { ok: true });
39
+ return;
40
+ }
41
+ if (url.pathname === '/readyz') {
42
+ writeJson(response, startup.error ? 503 : 200, {
43
+ ok: !startup.error,
44
+ vault: input.vaultPath,
45
+ agent: input.agent ?? startup.agent ?? '*',
46
+ ...(startup.error ? { error: startup.error } : {})
47
+ });
48
+ return;
49
+ }
50
+ if (url.pathname !== mcpPath) {
51
+ writeJson(response, 404, { error: 'Not found' });
52
+ return;
53
+ }
54
+ if (!isAuthorized(input.token, request.headers.authorization)) {
55
+ writeJson(response, 401, { error: 'Unauthorized' });
56
+ return;
57
+ }
58
+ try {
59
+ const mcpServer = createBrainlinkMcpServer();
60
+ const transport = new StreamableHTTPServerTransport({
61
+ sessionIdGenerator: undefined,
62
+ enableJsonResponse: true
63
+ });
64
+ await mcpServer.connect(transport);
65
+ await transport.handleRequest(request, response);
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ writeJson(response, 500, { error: message });
70
+ }
71
+ });
72
+ await new Promise((resolve, reject) => {
73
+ server.once('error', reject);
74
+ server.listen(input.port, input.host, () => {
75
+ server.off('error', reject);
76
+ resolve();
77
+ });
78
+ });
79
+ const address = server.address();
80
+ const port = typeof address === 'object' && address ? address.port : input.port;
81
+ const publicHost = input.host === '0.0.0.0' ? '127.0.0.1' : input.host;
82
+ const baseUrl = `http://${publicHost}:${port}`;
83
+ return {
84
+ url: `${baseUrl}${mcpPath}`,
85
+ healthUrl: `${baseUrl}/healthz`,
86
+ readyUrl: `${baseUrl}/readyz`,
87
+ close: () => new Promise((resolve, reject) => {
88
+ server.close((error) => {
89
+ if (error) {
90
+ reject(error);
91
+ return;
92
+ }
93
+ resolve();
94
+ });
95
+ })
96
+ };
97
+ };
@@ -58,6 +58,20 @@ Guardrails for benchmark acceptance are configured with `searchPack.guardrailMin
58
58
  `autoIndexOnWrite` (default: `true`) controls whether `add` and MCP write tools index right after writing.
59
59
  `autoCanonicalContextLinks` (default: `true`) controls whether CLI/MCP write tools add canonical `## Context Links` to inferred context hubs.
60
60
 
61
+ ## Central MCP Service
62
+
63
+ For clustered applications, run Brainlink as one central MCP service and let workloads connect to it over Streamable HTTP:
64
+
65
+ ```bash
66
+ BRAINLINK_MCP_TOKEN="change-me" brainlink mcp-server \
67
+ --vault /data/vault \
68
+ --host 0.0.0.0 \
69
+ --port 3333 \
70
+ --path /mcp
71
+ ```
72
+
73
+ The central service exposes `/mcp` for MCP clients plus `/healthz` and `/readyz` for platform probes. Use a Kubernetes `Service` for internal discovery, mount the Markdown vault through a persistent volume, and pass `Authorization: Bearer <token>` from clients when `BRAINLINK_MCP_TOKEN` or `--token` is configured.
74
+
61
75
  ## Agent Namespaces
62
76
 
63
77
  Each agent writes into a dedicated namespace under `agents/<agent-id>/`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.166",
3
+ "version": "0.1.0-beta.167",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,6 +48,7 @@
48
48
  "build": "npm run clean && npx --yes snyk test && tsc -p tsconfig.json",
49
49
  "dev": "tsx src/cli/main.ts",
50
50
  "dev:mcp": "tsx src/mcp/main.ts",
51
+ "dev:mcp:http": "tsx src/cli/main.ts mcp-server",
51
52
  "brainlink:sync": "bash scripts/brainlink-sync.sh",
52
53
  "security": "npx --yes snyk test",
53
54
  "test": "vitest run --config vitest.config.ts",